Merge m-c to birch. FIREFOX_AURORA_23_BASE
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 13 May 2013 09:53:32 -0400
changeset 131682 d7ce9089999719d5186595d160f25123a4e63e39
parent 131681 7bb17c15fb2754d6152dbfe9a662a1283a806f09 (current diff)
parent 131665 2e6cfac714f666381620f767bbe42f2595769f95 (diff)
child 131683 ec5790a0e0a261ac1646ae2e99aa520d2fdb4cec
child 131803 4f44cdf5aafc6986ceeadb6ab6030edc7a2ec046
push idunknown
push userunknown
push dateunknown
milestone23.0a1
Merge m-c to birch.
browser/devtools/commandline/gcli.jsm
browser/devtools/shared/Browser.jsm
browser/devtools/shared/Templater.jsm
browser/devtools/shared/test/browser_browser_basic.js
browser/devtools/webconsole/PropertyPanel.jsm
build/mobile/robocop/parse_ids.py
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1027,17 +1027,16 @@ pref("devtools.commands.dir", "");
 
 // Toolbox preferences
 pref("devtools.toolbox.footer.height", 250);
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]');
 pref("devtools.toolbox.sideEnabled", true);
-pref("devtools.toolbox.disabledTools", "[]");
 
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 pref("devtools.inspector.activeSidebar", "ruleview");
 pref("devtools.inspector.markupPreview", false);
 
 // Enable the Layout View
 pref("devtools.layoutview.enabled", true);
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -25,16 +25,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_dying_cache.js \
 	browser_form_restore_events.js \
 	browser_form_restore_events_sample.html \
 	browser_formdata_format.js \
 	browser_formdata_format_sample.html \
 	browser_input.js \
 	browser_input_sample.html \
 	browser_pageshow.js \
+	browser_windowRestore_perwindowpb.js \
 	browser_248970_b_perwindowpb.js \
 	browser_248970_b_sample.html \
 	browser_339445.js \
 	browser_339445_sample.html \
 	browser_345898.js \
 	browser_346337.js \
 	browser_346337_sample.html \
 	browser_350525.js \
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js
@@ -0,0 +1,30 @@
+/* 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/. */
+
+// This test checks that closed private windows can't be restored
+
+function test() {
+  waitForExplicitFinish();
+
+  // Purging the list of closed windows
+  while(ss.getClosedWindowCount() > 0)
+    ss.forgetClosedWindow(0);
+
+  // Load a private window, then close it 
+  // and verify it doesn't get remembered for restoring
+  var win = OpenBrowserWindow({private: true});
+
+  whenWindowLoaded(win, function onload() {
+    info("The private window got loaded");
+    win.addEventListener("SSWindowClosing", function onclosing() {
+      win.removeEventListener("SSWindowClosing", onclosing, false);
+      executeSoon(function () {
+        is (ss.getClosedWindowCount(), 0,
+            "The private window should not have been stored");
+        finish();
+      });
+    }, false);
+    win.close();
+  });
+}
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -4,22 +4,24 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
                            .getService(Ci.nsIStringBundleService)
                            .createBundle("chrome://branding/locale/brand.properties")
                            .GetStringFromName("brandShortName");
 
-this.EXPORTED_SYMBOLS = [ "CmdAddonFlags", "CmdCommands" ];
+this.EXPORTED_SYMBOLS = [ "CmdAddonFlags", "CmdCommands", "DEFAULT_DEBUG_PORT", "connect" ];
 
-Cu.import("resource:///modules/devtools/gcli.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/osfile.jsm")
+
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppCacheUtils",
                                   "resource:///modules/devtools/AppCacheUtils.jsm");
@@ -32,26 +34,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
   // We need to use an object in which to store any flags because a primitive
   // would remain undefined.
   module.CmdAddonFlags = {
     addonsLoaded: false
   };
 
   /**
-  * 'addon' command.
-  */
+   * 'addon' command.
+   */
   gcli.addCommand({
     name: "addon",
     description: gcli.lookup("addonDesc")
   });
 
   /**
-  * 'addon list' command.
-  */
+   * 'addon list' command.
+   */
   gcli.addCommand({
     name: "addon list",
     description: gcli.lookup("addonListDesc"),
     returnType: "addonsInfo",
     params: [{
       name: 'type',
       type: {
         name: 'selection',
@@ -256,21 +258,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     * 'addon enable' command.
     */
     gcli.addCommand({
       name: "addon enable",
       description: gcli.lookup("addonEnableDesc"),
       params: [nameParameter],
       exec: function(aArgs, context) {
         /**
-        * Enables the addon in the passed list which has a name that matches
-        * according to the passed name comparer, and resolves the promise which
-        * is the scope (this) of this function to display the result of this
-        * enable attempt.
-        */
+         * Enables the addon in the passed list which has a name that matches
+         * according to the passed name comparer, and resolves the promise which
+         * is the scope (this) of this function to display the result of this
+         * enable attempt.
+         */
         function enable(aName, addons) {
           // Find the add-on.
           let addon = null;
           addons.some(function(candidate) {
             if (candidate.name == aName) {
               addon = candidate;
               return true;
             } else {
@@ -293,18 +295,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
         let deferred = context.defer();
         // List the installed add-ons, enable one when done listing.
         AddonManager.getAllAddons(enable.bind(deferred, aArgs.name));
         return deferred.promise;
       }
     });
 
     /**
-    * 'addon disable' command.
-    */
+     * 'addon disable' command.
+     */
     gcli.addCommand({
       name: "addon disable",
       description: gcli.lookup("addonDisableDesc"),
       params: [nameParameter],
       exec: function(aArgs, context) {
         /**
         * Like enable, but ... you know ... the exact opposite.
         */
@@ -405,32 +407,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     JsDebugger.addDebuggerToGlobal(global);
 
     return global.Debugger;
   });
 
   let debuggers = [];
 
   /**
-  * 'calllog' command
-  */
+   * 'calllog' command
+   */
   gcli.addCommand({
     name: "calllog",
     description: gcli.lookup("calllogDesc")
   })
 
   /**
-  * 'calllog start' command
-  */
+   * 'calllog start' command
+   */
   gcli.addCommand({
     name: "calllog start",
     description: gcli.lookup("calllogStartDesc"),
 
     exec: function(args, context) {
-      let contentWindow = context.environment.contentDocument.defaultView;
+      let contentWindow = context.environment.window;
 
       let dbg = new Debugger(contentWindow);
       dbg.onEnterFrame = function(frame) {
         // BUG 773652 -  Make the output from the GCLI calllog command nicer
         contentWindow.console.log("Method call: " + this.callDescription(frame));
       }.bind(this);
 
       debuggers.push(dbg);
@@ -462,18 +464,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       if (typeof value !== "object" || value === null) {
         return uneval(value);
       }
       return "[object " + value.class + "]";
     }
   });
 
   /**
-  * 'calllog stop' command
-  */
+   * 'calllog stop' command
+   */
   gcli.addCommand({
     name: "calllog stop",
     description: gcli.lookup("calllogStopDesc"),
 
     exec: function(args, context) {
       let numDebuggers = debuggers.length;
       if (numDebuggers == 0) {
         return gcli.lookup("calllogStopNoLogging");
@@ -501,18 +503,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
     return global.Debugger;
   });
 
   let debuggers = [];
   let sandboxes = [];
 
   /**
-  * 'calllog chromestart' command
-  */
+   * 'calllog chromestart' command
+   */
   gcli.addCommand({
     name: "calllog chromestart",
     description: gcli.lookup("calllogChromeStartDesc"),
     get hidden() gcli.hiddenByChromePref(),
     params: [
       {
         name: "sourceType",
         type: {
@@ -524,17 +526,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
         name: "source",
         type: "string",
         description: gcli.lookup("calllogChromeSourceTypeDesc"),
         manual: gcli.lookup("calllogChromeSourceTypeManual"),
       }
     ],
     exec: function(args, context) {
       let globalObj;
-      let contentWindow = context.environment.contentDocument.defaultView;
+      let contentWindow = context.environment.window;
 
       if (args.sourceType == "jsm") {
         try {
           globalObj = Cu.import(args.source);
         }
         catch (e) {
           return gcli.lookup("callLogChromeInvalidJSM");
         }
@@ -603,18 +605,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     callDescription: function(frame) {
       let name = frame.callee.name || gcli.lookup("callLogChromeAnonFunction");
       let args = frame.arguments.map(this.valueToString).join(", ");
       return name + "(" + args + ")";
     }
   });
 
   /**
-  * 'calllog chromestop' command
-  */
+   * 'calllog chromestop' command
+   */
   gcli.addCommand({
     name: "calllog chromestop",
     description: gcli.lookup("calllogChromeStopDesc"),
     get hidden() gcli.hiddenByChromePref(),
     exec: function(args, context) {
       let numDebuggers = debuggers.length;
       if (numDebuggers == 0) {
         return gcli.lookup("calllogChromeStopNoLogging");
@@ -647,32 +649,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                     "resource://gre/modules/NetUtil.jsm");
   XPCOMUtils.defineLazyModuleGetter(this, "console",
                                     "resource://gre/modules/devtools/Console.jsm");
 
   const PREF_DIR = "devtools.commands.dir";
 
   /**
-  * A place to store the names of the commands that we have added as a result of
-  * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
-  * added commands.
-  */
+   * A place to store the names of the commands that we have added as a result of
+   * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
+   * added commands.
+   */
   let commands = [];
 
   /**
-  * Exported API
-  */
+   * Exported API
+   */
   this.CmdCommands = {
     /**
-    * Called to look in a directory pointed at by the devtools.commands.dir pref
-    * for *.mozcmd files which are then loaded.
-    * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
-    * we eval the script from the .mozcmd file. This should be a chrome window.
-    */
+     * Called to look in a directory pointed at by the devtools.commands.dir pref
+     * for *.mozcmd files which are then loaded.
+     * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+     * we eval the script from the .mozcmd file. This should be a chrome window.
+     */
     refreshAutoCommands: function GC_refreshAutoCommands(aSandboxPrincipal) {
       // First get rid of the last set of commands
       commands.forEach(function(name) {
         gcli.removeCommand(name);
       });
 
       let dirName = prefBranch.getComplexValue(PREF_DIR,
                                               Ci.nsISupportsString).data.trim();
@@ -729,22 +731,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
             }
           );
         }
       );
     }
   };
 
   /**
-  * Load the commands from a single file
-  * @param OS.File.DirectoryIterator.Entry aFileEntry The DirectoryIterator
-  * Entry of the file containing the commands that we should read
-  * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
-  * we eval the script from the .mozcmd file. This should be a chrome window.
-  */
+   * Load the commands from a single file
+   * @param OS.File.DirectoryIterator.Entry aFileEntry The DirectoryIterator
+   * Entry of the file containing the commands that we should read
+   * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+   * we eval the script from the .mozcmd file. This should be a chrome window.
+   */
   function loadCommandFile(aFileEntry, aSandboxPrincipal) {
     let promise = OS.File.read(aFileEntry.path);
     promise = promise.then(
       function onSuccess(array) {
         let decoder = new TextDecoder();
         let source = decoder.decode(array);
 
         let sandbox = new Cu.Sandbox(aSandboxPrincipal, {
@@ -768,27 +770,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       function onError(reason) {
         console.error("OS.File.read(" + aFileEntry.path + ") failed.");
         throw reason;
       }
     );
   }
 
   /**
-  * 'cmd' command
-  */
+   * 'cmd' command
+   */
   gcli.addCommand({
     name: "cmd",
     get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
     description: gcli.lookup("cmdDesc")
   });
 
   /**
-  * 'cmd refresh' command
-  */
+   * 'cmd refresh' command
+   */
   gcli.addCommand({
     name: "cmd refresh",
     description: gcli.lookup("cmdRefreshDesc"),
     get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
     exec: function Command_cmdRefresh(args, context) {
       let chromeWindow = context.environment.chromeDocument.defaultView;
       CmdCommands.refreshAutoCommands(chromeWindow);
     }
@@ -797,56 +799,55 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 /* CmdConsole -------------------------------------------------------------- */
 
 (function(module) {
   XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
                                     "resource:///modules/HUDService.jsm");
 
   /**
-  * 'console' command
-  */
+   * 'console' command
+   */
   gcli.addCommand({
     name: "console",
     description: gcli.lookup("consoleDesc"),
     manual: gcli.lookup("consoleManual")
   });
 
   /**
-  * 'console clear' command
-  */
+   * 'console clear' command
+   */
   gcli.addCommand({
     name: "console clear",
     description: gcli.lookup("consoleclearDesc"),
     exec: function Command_consoleClear(args, context) {
-      let window = context.environment.contentDocument.defaultView;
-      let hud = HUDService.getHudByWindow(window);
+      let hud = HUDService.getHudByWindow(context.environment.window);
       // hud will be null if the web console has not been opened for this window
       if (hud) {
         hud.jsterm.clearOutput();
       }
     }
   });
 
   /**
-  * 'console close' command
-  */
+   * 'console close' command
+   */
   gcli.addCommand({
     name: "console close",
     description: gcli.lookup("consolecloseDesc"),
     exec: function Command_consoleClose(args, context) {
       let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
       let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
       return gDevTools.closeToolbox(target);
     }
   });
 
   /**
-  * 'console open' command
-  */
+   * 'console open' command
+   */
   gcli.addCommand({
     name: "console open",
     description: gcli.lookup("consoleopenDesc"),
     exec: function Command_consoleOpen(args, context) {
       let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
       let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
       return gDevTools.showToolbox(target, "webconsole");
     }
@@ -971,17 +972,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
    */
   gcli.addCommand({
     name: "cookie list",
     description: gcli.lookup("cookieListDesc"),
     manual: gcli.lookup("cookieListManual"),
     returnType: "cookies",
     exec: function Command_cookieList(args, context) {
       let host = context.environment.document.location.host;
-      if (host == null) {
+      if (host == null || host == "") {
         throw new Error(gcli.lookup("cookieListOutNonePage"));
       }
 
       let enm = cookieMgr.getCookiesFromHost(host);
 
       let cookies = [];
       while (enm.hasMoreElements()) {
         let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
@@ -1018,17 +1019,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
     ],
     exec: function Command_cookieRemove(args, context) {
       let host = context.environment.document.location.host;
       let enm = cookieMgr.getCookiesFromHost(host);
 
       let cookies = [];
       while (enm.hasMoreElements()) {
-        let cookie = enm.getNext().QueryInterface(Components.interfaces.nsICookie);
+        let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
         if (isCookieAtHost(cookie, host)) {
           if (cookie.name == args.name) {
             cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
           }
         }
       }
     }
   });
@@ -1151,55 +1152,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           visible: true,
           typed: command
         });
       });
     }
   }
 }(this));
 
-/* CmdEcho ----------------------------------------------------------------- */
-
-(function(module) {
-  /**
-  * 'echo' command
-  */
-  gcli.addCommand({
-    name: "echo",
-    description: gcli.lookup("echoDesc"),
-    params: [
-      {
-        name: "message",
-        type: "string",
-        description: gcli.lookup("echoMessageDesc")
-      }
-    ],
-    returnType: "string",
-    hidden: true,
-    exec: function Command_echo(args, context) {
-      return args.message;
-    }
-  });
-}(this));
-
 /* CmdExport --------------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'export' command
-  */
+   * 'export' command
+   */
   gcli.addCommand({
     name: "export",
     description: gcli.lookup("exportDesc"),
   });
 
   /**
-  * The 'export html' command. This command allows the user to export the page to
-  * HTML after they do DOM changes.
-  */
+   * The 'export html' command. This command allows the user to export the page to
+   * HTML after they do DOM changes.
+   */
   gcli.addCommand({
     name: "export html",
     description: gcli.lookup("exportHtmlDesc"),
     exec: function(args, context) {
       let html = context.environment.document.documentElement.outerHTML;
       let url = 'data:text/plain;charset=utf8,' + encodeURIComponent(html);
       context.environment.window.open(url);
     }
@@ -1211,18 +1188,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 (function(module) {
   const XMLHttpRequest =
     Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
 
   XPCOMUtils.defineLazyModuleGetter(this, "js_beautify",
                                     "resource:///modules/devtools/Jsbeautify.jsm");
 
   /**
-  * jsb command.
-  */
+   * jsb command.
+   */
   gcli.addCommand({
     name: 'jsb',
     description: gcli.lookup('jsbDesc'),
     returnValue:'string',
     params: [
       {
         name: 'url',
         type: 'string',
@@ -1338,27 +1315,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     }
   });
 }(this));
 
 /* CmdPagemod -------------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'pagemod' command
-  */
+   * 'pagemod' command
+   */
   gcli.addCommand({
     name: "pagemod",
     description: gcli.lookup("pagemodDesc"),
   });
 
   /**
-  * The 'pagemod replace' command. This command allows the user to search and
-  * replace within text nodes and attributes.
-  */
+   * The 'pagemod replace' command. This command allows the user to search and
+   * replace within text nodes and attributes.
+   */
   gcli.addCommand({
     name: "pagemod replace",
     description: gcli.lookup("pagemodReplaceDesc"),
     params: [
       {
         name: "search",
         type: "string",
         description: gcli.lookup("pagemodReplaceSearchDesc"),
@@ -1398,27 +1375,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       {
         name: "attributes",
         type: "string",
         description: gcli.lookup("pagemodReplaceAttributesDesc"),
         defaultValue: null,
       },
     ],
     exec: function(args, context) {
-      let document = context.environment.contentDocument;
       let searchTextNodes = !args.attrOnly;
       let searchAttributes = !args.contentOnly;
       let regexOptions = args.ignoreCase ? 'ig' : 'g';
       let search = new RegExp(escapeRegex(args.search), regexOptions);
       let attributeRegex = null;
       if (args.attributes) {
         attributeRegex = new RegExp(args.attributes, regexOptions);
       }
 
-      let root = args.root || document;
+      let root = args.root || context.environment.document;
       let elements = root.querySelectorAll(args.selector);
       elements = Array.prototype.slice.call(elements);
 
       let replacedTextNodes = 0;
       let replacedAttributes = 0;
 
       function replaceAttribute() {
         replacedAttributes++;
@@ -1455,27 +1431,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
       return gcli.lookupFormat("pagemodReplaceResult",
                               [elements.length, replacedTextNodes,
                                 replacedAttributes]);
     }
   });
 
   /**
-  * 'pagemod remove' command
-  */
+   * 'pagemod remove' command
+   */
   gcli.addCommand({
     name: "pagemod remove",
     description: gcli.lookup("pagemodRemoveDesc"),
   });
 
 
   /**
-  * The 'pagemod remove element' command.
-  */
+   * The 'pagemod remove element' command.
+   */
   gcli.addCommand({
     name: "pagemod remove element",
     description: gcli.lookup("pagemodRemoveElementDesc"),
     params: [
       {
         name: "search",
         type: "string",
         description: gcli.lookup("pagemodRemoveElementSearchDesc"),
@@ -1493,18 +1469,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       },
       {
         name: 'ifEmptyOnly',
         type: 'boolean',
         description: gcli.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
       },
     ],
     exec: function(args, context) {
-      let document = context.environment.contentDocument;
-      let root = args.root || document;
+      let root = args.root || context.environment.document;
       let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
 
       let removed = 0;
       for (let i = 0; i < elements.length; i++) {
         let element = elements[i];
         let parentNode = element.parentNode;
         if (!parentNode || !element.removeChild) {
           continue;
@@ -1521,18 +1496,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
 
       return gcli.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
                               [elements.length, removed]);
     }
   });
 
   /**
-  * The 'pagemod remove attribute' command.
-  */
+   * The 'pagemod remove attribute' command.
+   */
   gcli.addCommand({
     name: "pagemod remove attribute",
     description: gcli.lookup("pagemodRemoveAttributeDesc"),
     params: [
       {
         name: "searchAttributes",
         type: "string",
         description: gcli.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
@@ -1550,19 +1525,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       },
       {
         name: "ignoreCase",
         type: "boolean",
         description: gcli.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
       },
     ],
     exec: function(args, context) {
-      let document = context.environment.contentDocument;
-
-      let root = args.root || document;
+      let root = args.root || context.environment.document;
       let regexOptions = args.ignoreCase ? 'ig' : 'g';
       let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
       let elements = root.querySelectorAll(args.searchElements);
       elements = Array.prototype.slice.call(elements);
 
       let removed = 0;
       for (let i = 0; i < elements.length; i++) {
         let element = elements[i];
@@ -1581,23 +1554,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
 
       return gcli.lookupFormat("pagemodRemoveAttributeResult",
                               [elements.length, removed]);
     }
   });
 
   /**
-  * Make a given string safe to use  in a regular expression.
-  *
-  * @param string aString
-  *        The string you want to use in a regex.
-  * @return string
-  *         The equivalent of |aString| but safe to use in a regex.
-  */
+   * Make a given string safe to use  in a regular expression.
+   *
+   * @param string aString
+   *        The string you want to use in a regex.
+   * @return string
+   *         The equivalent of |aString| but safe to use in a regex.
+   */
   function escapeRegex(aString) {
     return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
   }
 }(this));
 
 /* CmdTools -------------------------------------------------------------- */
 
 (function(module) {
@@ -1617,32 +1590,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       {
         name: "srcdir",
         type: "string",
         description: gcli.lookup("toolsSrcdirDir")
       }
     ],
     returnType: "string",
     exec: function(args, context) {
-      let promise = context.createPromise();
-      let existsPromise = OS.File.exists(args.srcdir + "/CLOBBER");
-      existsPromise.then(function(exists) {
+      return OS.File.exists(args.srcdir + "/CLOBBER").then(function(exists) {
         if (exists) {
-          var str = Cc["@mozilla.org/supports-string;1"]
-            .createInstance(Ci.nsISupportsString);
+          let str = Cc["@mozilla.org/supports-string;1"]
+                    .createInstance(Ci.nsISupportsString);
           str.data = args.srcdir;
           Services.prefs.setComplexValue("devtools.loader.srcdir",
-              Components.interfaces.nsISupportsString, str);
+                                         Ci.nsISupportsString, str);
           devtools.reload();
-          promise.resolve(gcli.lookupFormat("toolsSrcdirReloaded", [args.srcdir]));
-          return;
+
+          let msg = gcli.lookupFormat("toolsSrcdirReloaded", [args.srcdir]);
+          throw new Error(msg);
         }
-        promise.reject(gcli.lookupFormat("toolsSrcdirNotFound", [args.srcdir]));
+
+        return gcli.lookupFormat("toolsSrcdirNotFound", [args.srcdir]);
       });
-      return promise;
     }
   });
 
   gcli.addCommand({
     name: "tools builtin",
     description: gcli.lookup("toolsBuiltinDesc"),
     manual: gcli.lookup("toolsBuiltinManual"),
     get hidden() gcli.hiddenByChromePref(),
@@ -1666,28 +1638,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     }
   });
 }(this));
 
 /* CmdRestart -------------------------------------------------------------- */
 
 (function(module) {
   /**
-  * Restart command
-  *
-  * @param boolean nocache
-  *        Disables loading content from cache upon restart.
-  *
-  * Examples :
-  * >> restart
-  * - restarts browser immediately
-  * >> restart --nocache
-  * - restarts immediately and starts Firefox without using cache
-  */
-
+   * Restart command
+   *
+   * @param boolean nocache
+   *        Disables loading content from cache upon restart.
+   *
+   * Examples :
+   * >> restart
+   * - restarts browser immediately
+   * >> restart --nocache
+   * - restarts immediately and starts Firefox without using cache
+   */
   gcli.addCommand({
     name: "restart",
     description: gcli.lookupFormat("restartBrowserDesc", [BRAND_SHORT_NAME]),
     params: [
       {
         name: "nocache",
         type: "boolean",
         description: gcli.lookup("restartBrowserNocacheDesc")
@@ -1722,23 +1693,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   XPCOMUtils.defineLazyModuleGetter(this, "LayoutHelpers",
                                     "resource:///modules/devtools/LayoutHelpers.jsm");
 
   // String used as an indication to generate default file name in the following
   // format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
   const FILENAME_DEFAULT_VALUE = " ";
 
   /**
-  * 'screenshot' command
-  */
+   * 'screenshot' command
+   */
   gcli.addCommand({
     name: "screenshot",
     description: gcli.lookup("screenshotDesc"),
     manual: gcli.lookup("screenshotManual"),
-    returnType: "html",
+    returnType: "dom",
     params: [
       {
         name: "filename",
         type: "string",
         defaultValue: FILENAME_DEFAULT_VALUE,
         description: gcli.lookup("screenshotFilenameDesc"),
         manual: gcli.lookup("screenshotFilenameManual")
       },
@@ -1783,34 +1754,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     exec: function Command_screenshot(args, context) {
       if (args.chrome && args.selector) {
         // Node screenshot with chrome option does not work as inteded
         // Refer https://bugzilla.mozilla.org/show_bug.cgi?id=659268#c7
         // throwing for now.
         throw new Error(gcli.lookup("screenshotSelectorChromeConflict"));
       }
       var document = args.chrome? context.environment.chromeDocument
-                                : context.environment.contentDocument;
+                                : context.environment.document;
       if (args.delay > 0) {
         var deferred = context.defer();
         document.defaultView.setTimeout(function Command_screenshotDelay() {
           let reply = this.grabScreen(document, args.filename, args.clipboard,
                                       args.fullpage);
           deferred.resolve(reply);
         }.bind(this), args.delay * 1000);
         return deferred.promise;
       }
       else {
         return this.grabScreen(document, args.filename, args.clipboard,
                               args.fullpage, args.selector);
       }
     },
-    grabScreen:
-    function Command_screenshotGrabScreen(document, filename, clipboard,
-                                          fullpage, node) {
+    grabScreen: function(document, filename, clipboard, fullpage, node) {
       let window = document.defaultView;
       let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
       let left = 0;
       let top = 0;
       let width;
       let height;
       let div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
 
@@ -1943,23 +1912,63 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                         "background-size: 256px " + previewHeight + "px;" +
                         "margin: 4px; display: block");
       div.appendChild(image);
       return div;
     }
   });
 }(this));
 
+
+/* Remoting ----------------------------------------------------------- */
+
+const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
+/**
+ * 'listen' command
+ */
+gcli.addCommand({
+  name: "listen",
+  description: gcli.lookup("listenDesc"),
+  manual: gcli.lookup("listenManual"),
+  params: [
+    {
+      name: "port",
+      type: "number",
+      get defaultValue() {
+        return Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+      },
+      description: gcli.lookup("listenPortDesc"),
+    }
+  ],
+  exec: function Command_screenshot(args, context) {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors();
+    }
+    var reply = DebuggerServer.openListener(args.port);
+    if (!reply) {
+      throw new Error(gcli.lookup("listenDisabledOutput"));
+    }
+
+    if (DebuggerServer.initialized) {
+      return gcli.lookupFormat("listenInitOutput", [ '' + args.port ]);
+    }
+
+    return gcli.lookup("listenNoInitOutput");
+  },
+});
+
+
 /* CmdPaintFlashing ------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'paintflashing' command
-  */
-
+   * 'paintflashing' command
+   */
   gcli.addCommand({
     name: 'paintflashing',
     description: gcli.lookup('paintflashingDesc')
   });
 
   gcli.addCommand({
     name: 'paintflashing on',
     description: gcli.lookup('paintflashingOnDesc'),
@@ -1971,25 +1980,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           type: "boolean",
           name: "chrome",
           get hidden() gcli.hiddenByChromePref(),
           description: gcli.lookup("paintflashingChromeDesc"),
         }
       ]
     }],
     exec: function(args, context) {
-      var window;
-      if (args.chrome) {
-        window = context.environment.chromeDocument.defaultView;
-      } else {
-        window = context.environment.contentDocument.defaultView;
-      }
-      window.QueryInterface(Ci.nsIInterfaceRequestor).
-             getInterface(Ci.nsIDOMWindowUtils).
-             paintFlashing = true;
+      var window = args.chrome ?
+                  context.environment.chromeWindow :
+                  context.environment.window;
+
+      window.QueryInterface(Ci.nsIInterfaceRequestor)
+            .getInterface(Ci.nsIDOMWindowUtils)
+            .paintFlashing = true;
       onPaintFlashingChanged(context);
     }
   });
 
   gcli.addCommand({
     name: 'paintflashing off',
     description: gcli.lookup('paintflashingOffDesc'),
     manual: gcli.lookup('paintflashingManual'),
@@ -2000,24 +2007,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           type: "boolean",
           name: "chrome",
           get hidden() gcli.hiddenByChromePref(),
           description: gcli.lookup("paintflashingChromeDesc"),
         }
       ]
     }],
     exec: function(args, context) {
-      if (args.chrome) {
-        var window = context.environment.chromeDocument.defaultView;
-      } else {
-        var window = context.environment.contentDocument.defaultView;
-      }
-      window.QueryInterface(Ci.nsIInterfaceRequestor).
-             getInterface(Ci.nsIDOMWindowUtils).
-             paintFlashing = false;
+      var window = args.chrome ?
+                  context.environment.chromeWindow :
+                  context.environment.window;
+
+      window.QueryInterface(Ci.nsIInterfaceRequestor)
+            .getInterface(Ci.nsIDOMWindowUtils)
+            .paintFlashing = false;
       onPaintFlashingChanged(context);
     }
   });
 
   gcli.addCommand({
     name: 'paintflashing toggle',
     hidden: true,
     buttonId: "command-button-paintflashing",
@@ -2036,17 +2042,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       onChange: function(aTarget, aChangeHandler) {
         eventEmitter.on("changed", aChangeHandler);
       },
       offChange: function(aTarget, aChangeHandler) {
         eventEmitter.off("changed", aChangeHandler);
       },
     },
     tooltipText: gcli.lookup("paintflashingTooltip"),
-    description: gcli.lookup('paintflashingOnDesc'),
+    description: gcli.lookup('paintflashingToggleDesc'),
     manual: gcli.lookup('paintflashingManual'),
     exec: function(args, context) {
       var gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
       var window = gBrowser.contentWindow;
       var wUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).
                    getInterface(Ci.nsIDOMWindowUtils);
       wUtils.paintFlashing = !wUtils.paintFlashing;
       onPaintFlashingChanged(context);
@@ -2066,18 +2072,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     target.once("navigate", fireChange);
   }
 }(this));
 
 /* CmdAppCache ------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'appcache' command
-  */
+   * 'appcache' command
+   */
 
   gcli.addCommand({
     name: 'appcache',
     description: gcli.lookup('appCacheDesc')
   });
 
   gcli.addConverter({
     from: "appcacheerrors",
@@ -2120,101 +2126,88 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           name: "uri",
           description: gcli.lookup("appCacheValidateUriDesc"),
           defaultValue: null,
         }
       ]
     }],
     exec: function(args, context) {
       let utils;
-      let promise = context.createPromise();
+      let deferred = context.defer();
 
       if (args.uri) {
         utils = new AppCacheUtils(args.uri);
       } else {
-        let doc = context.environment.contentDocument;
-        utils = new AppCacheUtils(doc);
+        utils = new AppCacheUtils(context.environment.document);
       }
 
       utils.validateManifest().then(function(errors) {
-        promise.resolve([errors, utils.manifestURI || "-"]);
+        deferred.resolve([errors, utils.manifestURI || "-"]);
       });
 
-      return promise;
+      return deferred.promise;
     }
   });
 
   gcli.addCommand({
     name: 'appcache clear',
     description: gcli.lookup('appCacheClearDesc'),
     manual: gcli.lookup('appCacheClearManual'),
     exec: function(args, context) {
       let utils = new AppCacheUtils(args.uri);
       utils.clearAll();
 
       return gcli.lookup("appCacheClearCleared");
     }
   });
 
+  let appcacheListEntries = "" +
+    "<ul class='gcli-appcache-list'>" +
+    "  <li foreach='entry in ${entries}'>" +
+    "    <table class='gcli-appcache-detail'>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListKey") + "</td>" +
+    "        <td>${entry.key}</td>" +
+    "      </tr>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListFetchCount") + "</td>" +
+    "        <td>${entry.fetchCount}</td>" +
+    "      </tr>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListLastFetched") + "</td>" +
+    "        <td>${entry.lastFetched}</td>" +
+    "      </tr>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListLastModified") + "</td>" +
+    "        <td>${entry.lastModified}</td>" +
+    "      </tr>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListExpirationTime") + "</td>" +
+    "        <td>${entry.expirationTime}</td>" +
+    "      </tr>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListDataSize") + "</td>" +
+    "        <td>${entry.dataSize}</td>" +
+    "      </tr>" +
+    "      <tr>" +
+    "        <td>" + gcli.lookup("appCacheListDeviceID") + "</td>" +
+    "        <td>${entry.deviceID} <span class='gcli-out-shortcut' " +
+    "onclick='${onclick}' ondblclick='${ondblclick}' " +
+    "data-command='appcache viewentry ${entry.key}'" +
+    ">" + gcli.lookup("appCacheListViewEntry") + "</span>" +
+    "        </td>" +
+    "      </tr>" +
+    "    </table>" +
+    "  </li>" +
+    "</ul>";
+
   gcli.addConverter({
     from: "appcacheentries",
     to: "view",
     exec: function(entries, context) {
-      if (!entries) {
-        return context.createView({
-          html: "<span>" + gcli.lookup("appCacheManifestContainsErrors") + "</span>"
-        });
-      }
-
-      if (entries.length == 0) {
-        return context.createView({
-          html: "<span>" + gcli.lookup("appCacheNoResults") + "</span>"
-        });
-      }
-
-      let appcacheListEntries = "" +
-        "<ul class='gcli-appcache-list'>" +
-        "  <li foreach='entry in ${entries}'>" +
-        "    <table class='gcli-appcache-detail'>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListKey") + "</td>" +
-        "        <td>${entry.key}</td>" +
-        "      </tr>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListFetchCount") + "</td>" +
-        "        <td>${entry.fetchCount}</td>" +
-        "      </tr>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListLastFetched") + "</td>" +
-        "        <td>${entry.lastFetched}</td>" +
-        "      </tr>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListLastModified") + "</td>" +
-        "        <td>${entry.lastModified}</td>" +
-        "      </tr>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListExpirationTime") + "</td>" +
-        "        <td>${entry.expirationTime}</td>" +
-        "      </tr>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListDataSize") + "</td>" +
-        "        <td>${entry.dataSize}</td>" +
-        "      </tr>" +
-        "      <tr>" +
-        "        <td>" + gcli.lookup("appCacheListDeviceID") + "</td>" +
-        "        <td>${entry.deviceID} <span class='gcli-out-shortcut' " +
-        "onclick='${onclick}' ondblclick='${ondblclick}' " +
-        "data-command='appcache viewentry ${entry.key}'" +
-        ">" + gcli.lookup("appCacheListViewEntry") + "</span>" +
-        "        </td>" +
-        "      </tr>" +
-        "    </table>" +
-        "  </li>" +
-        "</ul>";
-
       return context.createView({
         html: appcacheListEntries,
         data: {
           entries: entries,
           onclick: createUpdateHandler(context),
           ondblclick: createExecuteHandler(context),
         }
       });
@@ -2233,44 +2226,36 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           type: "string",
           name: "search",
           description: gcli.lookup("appCacheListSearchDesc"),
           defaultValue: null,
         },
       ]
     }],
     exec: function(args, context) {
-      let doc = context.environment.contentDocument;
       let utils = new AppCacheUtils();
-
-      let entries = utils.listEntries(args.search);
-      return entries;
+      return utils.listEntries(args.search);
     }
   });
 
   gcli.addCommand({
     name: 'appcache viewentry',
     description: gcli.lookup('appCacheViewEntryDesc'),
     manual: gcli.lookup('appCacheViewEntryManual'),
     params: [
       {
         type: "string",
         name: "key",
         description: gcli.lookup("appCacheViewEntryKey"),
         defaultValue: null,
       }
     ],
     exec: function(args, context) {
-      let doc = context.environment.contentDocument;
       let utils = new AppCacheUtils();
-
-      let result = utils.viewEntry(args.key);
-      if (result) {
-        return result;
-      }
+      return utils.viewEntry(args.key);
     }
   });
 
   /**
    * Helper to find the 'data-command' attribute and call some action on it.
    * @see |updateCommand()| and |executeCommand()|
    */
   function withCommand(element, action) {
--- a/browser/devtools/commandline/test/Makefile.in
+++ b/browser/devtools/commandline/test/Makefile.in
@@ -26,16 +26,17 @@ MOCHITEST_BROWSER_FILES = \
   browser_cmd_appcache_valid.js \
   browser_cmd_appcache_valid_appcache.appcache \
   browser_cmd_appcache_valid_appcache.appcache^headers^ \
   browser_cmd_appcache_valid_index.html \
   browser_cmd_appcache_valid_page1.html \
   browser_cmd_appcache_valid_page2.html \
   browser_cmd_appcache_valid_page3.html \
   browser_cmd_commands.js \
+  browser_cmd_cookie.html \
   browser_cmd_cookie.js \
   browser_cmd_jsb.js \
   browser_cmd_jsb_script.jsi \
   browser_cmd_pagemod_export.html \
   browser_cmd_pagemod_export.js \
   browser_cmd_pref.js \
   browser_cmd_restart.js \
   browser_cmd_screenshot.html \
--- a/browser/devtools/commandline/test/browser_cmd_addon.js
+++ b/browser/devtools/commandline/test/browser_cmd_addon.js
@@ -11,23 +11,18 @@ function test() {
   helpers.addTabWithToolbar("about:blank", function(options) {
     return helpers.runTests(options, tests);
   }).then(finish);
 }
 
 tests.gatTest = function(options) {
   let deferred = Promise.defer();
 
-  // hack to reduce stack size as a result of bug 842347
-  let onGatReadyInterjection = function() {
-    executeSoon(onGatReady);
-  };
-
   let onGatReady = function() {
-    Services.obs.removeObserver(onGatReadyInterjection, "gcli_addon_commands_ready");
+    Services.obs.removeObserver(onGatReady, "gcli_addon_commands_ready");
     info("gcli_addon_commands_ready notification received, running tests");
 
     let auditDone = helpers.audit(options, [
       {
         setup: 'addon list dictionary',
         check: {
           input:  'addon list dictionary',
           hints:                       '',
@@ -116,17 +111,17 @@ tests.gatTest = function(options) {
       }
     ]);
 
     auditDone.then(function() {
       deferred.resolve();
     });
   };
 
-  Services.obs.addObserver(onGatReadyInterjection, "gcli_addon_commands_ready", false);
+  Services.obs.addObserver(onGatReady, "gcli_addon_commands_ready", false);
 
   if (CmdAddonFlags.addonsLoaded) {
     info("The call to AddonManager.getAllAddons in BuiltinCommands.jsm is done.");
     info("Send the gcli_addon_commands_ready notification ourselves.");
 
     Services.obs.notifyObservers(null, "gcli_addon_commands_ready", null);
   } else {
     info("Waiting for gcli_addon_commands_ready notification.");
--- a/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
+++ b/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
@@ -26,16 +26,35 @@ function test() {
             input:  'appcache',
             markup: 'IIIIIIII',
             status: 'ERROR',
             args: {}
           },
         },
 
         {
+          setup: function() {
+            Services.prefs.setBoolPref("browser.cache.disk.enable", false);
+            helpers.setInput(options, 'appcache list', 13);
+          },
+          check: {
+            input:  'appcache list',
+            markup: 'VVVVVVVVVVVVV',
+            status: 'VALID',
+            args: {},
+          },
+          exec: {
+            output: [ /cache is disabled/ ]
+          },
+          post: function(output) {
+            Services.prefs.setBoolPref("browser.cache.disk.enable", true);
+          }
+        },
+
+        {
           setup: 'appcache list',
           check: {
             input:  'appcache list',
             markup: 'VVVVVVVVVVVVV',
             status: 'VALID',
             args: {},
           },
           exec: {
@@ -51,18 +70,18 @@ function test() {
             status: 'VALID',
             args: {
               search: { value: 'page' },
             }
           },
           exec: {
             output: [ /page1/, /page2/, /page3/ ]
           },
-          post: function(output) {
-            ok(!output.contains("index"), "index is not contained in output");
+          post: function(output, text) {
+            ok(!text.contains("index"), "index is not contained in output");
           }
         },
 
         {
           setup: 'appcache validate',
           check: {
             input:  'appcache validate',
             markup: 'VVVVVVVVVVVVVVVVV',
@@ -113,21 +132,21 @@ function test() {
             input:  'appcache list',
             markup: 'VVVVVVVVVVVVV',
             status: 'VALID',
             args: {},
           },
           exec: {
             output: [ /no results/ ]
           },
-          post: function(output) {
-            ok(!output.contains("index"), "index is not contained in output");
-            ok(!output.contains("page1"), "page1 is not contained in output");
-            ok(!output.contains("page2"), "page1 is not contained in output");
-            ok(!output.contains("page3"), "page1 is not contained in output");
+          post: function(output, text) {
+            ok(!text.contains("index"), "index is not contained in output");
+            ok(!text.contains("page1"), "page1 is not contained in output");
+            ok(!text.contains("page2"), "page1 is not contained in output");
+            ok(!text.contains("page3"), "page1 is not contained in output");
           }
         },
 
         {
           setup: 'appcache viewentry --key ' + TEST_URI,
           check: {
             input:  'appcache viewentry --key ' + TEST_URI,
                   // appcache viewentry --key http://sub1.test2.example.com/browser/browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html
--- a/browser/devtools/commandline/test/browser_cmd_commands.js
+++ b/browser/devtools/commandline/test/browser_cmd_commands.js
@@ -10,27 +10,16 @@ const TEST_URI = "data:text/html;charset
 let tests = {};
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.runTests(options, tests);
   }).then(finish);
 }
 
-tests.testEcho = function(options) {
-  return helpers.audit(options, [
-    {
-      setup: "echo message",
-      exec: {
-        output: "message",
-      }
-    }
-  ]);
-};
-
 tests.testConsole = function(options) {
   let deferred = Promise.defer();
   let hud = null;
 
   let onWebConsoleOpen = function(subject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     subject.QueryInterface(Ci.nsISupportsString);
@@ -62,22 +51,17 @@ tests.testConsole = function(options) {
         exec: {
           output: ""
         },
         post: function() {
           ok(!(hud.hudId in HUDService.hudReferences), "console closed");
         }
       }
     ]).then(function() {
-      // FIXME: Remove this hack once bug 842347 is fixed
-      // Gak - our promises impl causes so many stack frames that we blow up the
-      // JS engine. Jumping to a new event with a truncated stack solves this.
-      executeSoon(function() {
-        deferred.resolve();
-      });
+      deferred.resolve();
     });
   };
 
   helpers.audit(options, [
     {
       setup: "console open",
       exec: { }
     }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_cookie.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>GCLI cookie command test</title>
+</head>
+<body>
+
+  <p>Cookie test</p>
+  <p id=result></p>
+  <script type="text/javascript">
+    document.cookie = "zap=zep";
+    document.cookie = "zip=zop";
+    document.getElementById("result").innerHTML = document.cookie;
+  </script>
+
+</body>
+</html>
--- a/browser/devtools/commandline/test/browser_cmd_cookie.js
+++ b/browser/devtools/commandline/test/browser_cmd_cookie.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the cookie commands works as they should
 
-const TEST_URI = "data:text/html;charset=utf-8,gcli-cookie";
+const TEST_URI = "http://example.com/browser/browser/devtools/commandline/"+
+                 "test/browser_cmd_cookie.html";
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.audit(options, [
       {
         setup: 'cookie',
         check: {
           input:  'cookie',
@@ -74,49 +75,81 @@ function test() {
             value: { value: 'ban' },
             secure: { value: false },
           }
         },
       },
       {
         setup: "cookie list",
         exec: {
-          output: 'No cookies found for host'
+          output: [ /zap=zep/, /zip=zop/, /Edit/ ]
         }
       },
       {
-        setup: "cookie set fruit banana",
+        setup: "cookie set zup banana",
         check: {
           args: {
-            name: { value: 'fruit' },
+            name: { value: 'zup' },
             value: { value: 'banana' },
           }
         },
         exec: {
           output: ""
         }
       },
       {
         setup: "cookie list",
         exec: {
-          output: [ /fruit=banana/, /Expires:/, /Edit/ ]
+          output: [ /zap=zep/, /zip=zop/, /zup=banana/, /Edit/ ]
         }
       },
       {
-        setup: "cookie remove fruit",
-        check: {
-          args: {
-            name: { value: 'fruit' },
-          }
+        setup: "cookie remove zip",
+        exec: { },
+      },
+      {
+        setup: "cookie list",
+        exec: {
+          output: [ /zap=zep/, /zup=banana/, /Edit/ ]
         },
-        exec: {
-          output: ""
+        post: function(output, text) {
+          ok(!text.contains("zip"), "");
+          ok(!text.contains("zop"), "");
         }
       },
       {
+        setup: "cookie remove zap",
+        exec: { },
+      },
+      {
         setup: "cookie list",
         exec: {
-          output: 'No cookies found for host'
+          output: [ /zup=banana/, /Edit/ ]
+        },
+        post: function(output, text) {
+          ok(!text.contains("zap"), "");
+          ok(!text.contains("zep"), "");
+          ok(!text.contains("zip"), "");
+          ok(!text.contains("zop"), "");
+        }
+      },
+      {
+        setup: "cookie remove zup",
+        exec: { }
+      },
+      {
+        setup: "cookie list",
+        exec: {
+          output: 'No cookies found for host example.com'
+        },
+        post: function(output, text) {
+          ok(!text.contains("zap"), "");
+          ok(!text.contains("zep"), "");
+          ok(!text.contains("zip"), "");
+          ok(!text.contains("zop"), "");
+          ok(!text.contains("zup"), "");
+          ok(!text.contains("banana"), "");
+          ok(!text.contains("Edit"), "");
         }
       },
     ]);
   }).then(finish);
 }
--- a/browser/devtools/commandline/test/browser_gcli_canon.js
+++ b/browser/devtools/commandline/test/browser_gcli_canon.js
@@ -33,16 +33,17 @@ function test() {
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var helpers = require('gclitest/helpers');
 var canon = require('gcli/canon');
 // var assert = require('test/assert');
+var Canon = canon.Canon;
 
 var startCount = undefined;
 var events = undefined;
 
 var canonChange = function(ev) {
   events++;
 };
 
@@ -152,18 +153,16 @@ exports.testAddRemove2 = function(option
   assert.is(canon.getCommands().length,
             startCount + 1,
             'rereadd command success');
   assert.is(events, 4, 'rereadd event');
 
   return helpers.audit(options, [
     {
       setup: 'testadd',
-      check: {
-      },
       exec: {
         output: /^3$/
       },
       post: function() {
         canon.removeCommand({
           name: 'testadd'
         });
 
@@ -194,9 +193,79 @@ exports.testAddRemove3 = function(option
   assert.is(canon.getCommands().length,
             startCount,
             'nonexistant2 command success');
   assert.is(events, 5, 'nonexistant2 event');
 
   canon.onCanonChange.remove(canonChange);
 };
 
+exports.testAltCanon = function(options) {
+  var altCanon = new Canon();
+
+  var tss = {
+    name: 'tss',
+    params: [
+      { name: 'str', type: 'string' },
+      { name: 'num', type: 'number' },
+      { name: 'opt', type: { name: 'selection', data: [ '1', '2', '3' ] } },
+    ],
+    exec: function(args, context) {
+      return context.commandName + ':' +
+              args.str + ':' + args.num + ':' + args.opt;
+    }
+  };
+  altCanon.addCommand(tss);
+
+  var commandSpecs = altCanon.getCommandSpecs();
+  assert.is(JSON.stringify(commandSpecs),
+            '{"tss":{"name":"tss","params":[' +
+              '{"name":"str","type":"string"},' +
+              '{"name":"num","type":"number"},' +
+              '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}]}}',
+            'JSON.stringify(commandSpecs)');
+
+  var remoter = function(args, context) {
+    assert.is(context.commandName, 'tss', 'commandName is tss');
+
+    var cmd = altCanon.getCommand(context.commandName);
+    return cmd.exec(args, context);
+  };
+
+  canon.addProxyCommands('proxy', commandSpecs, remoter, 'test');
+
+  var parent = canon.getCommand('proxy');
+  assert.is(parent.name, 'proxy', 'Parent command called proxy');
+
+  var child = canon.getCommand('proxy tss');
+  assert.is(child.name, 'proxy tss', 'child command called proxy tss');
+
+  return helpers.audit(options, [
+    {
+      setup:    'proxy tss foo 6 3',
+      check: {
+        input:  'proxy tss foo 6 3',
+        hints:                    '',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        cursor: 17,
+        status: 'VALID',
+        args: {
+          str: { value: 'foo', status: 'VALID' },
+          num: { value: 6, status: 'VALID' },
+          opt: { value: '3', status: 'VALID' }
+        }
+      },
+      exec: {
+        output: 'tss:foo:6:3'
+      },
+      post: function() {
+        canon.removeCommand('proxy');
+        canon.removeCommand('proxy tss');
+
+        assert.is(canon.getCommand('proxy'), undefined, 'remove proxy');
+        assert.is(canon.getCommand('proxy tss'), undefined, 'remove proxy tss');
+      }
+    }
+  ]);
+};
+
+
 // });
--- a/browser/devtools/commandline/test/browser_gcli_cli.js
+++ b/browser/devtools/commandline/test/browser_gcli_cli.js
@@ -753,17 +753,16 @@ exports.testSingleFloat = function(optio
       check: {
         input:  'tsf',
         hints:     ' <num>',
         markup: 'VVV',
         cursor: 3,
         current: '__command',
         status: 'ERROR',
         error: '',
-        predictions: [ ],
         unassigned: [ ],
         args: {
           command: { name: 'tsf' },
           num: {
             value: undefined,
             arg: '',
             status: 'INCOMPLETE',
             message: ''
--- a/browser/devtools/commandline/test/browser_gcli_completion.js
+++ b/browser/devtools/commandline/test/browser_gcli_completion.js
@@ -514,19 +514,19 @@ exports.testSpaceComplete = function(opt
         current: 'sel2',
         status: 'ERROR',
         tooltipState: 'true:importantFieldFlag',
         args: {
           command: { name: 'tslong' },
           msg: { status: 'INCOMPLETE', message: '' },
           num: { status: 'VALID' },
           sel: { status: 'VALID' },
-          bool: { value: false,status: 'VALID' },
+          bool: { value: false, status: 'VALID' },
           num2: { status: 'VALID' },
-          bool2: { value: false,status: 'VALID' },
+          bool2: { value: false, status: 'VALID' },
           sel2: {
             value: 'with space',
             arg: ' --sel2 \'with space\' ',
             status: 'VALID'
           }
         }
       }
     }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_context.js
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testContext.js</p>";
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URI, function(options) {
+    return helpers.runTests(options, exports);
+  }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var cli = require('gcli/cli');
+
+var origLogErrors = undefined;
+
+exports.setup = function(options) {
+  mockCommands.setup();
+
+  origLogErrors = cli.logErrors;
+  cli.logErrors = false;
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+
+  cli.logErrors = origLogErrors;
+  origLogErrors = undefined;
+};
+
+exports.testBaseline = function(options) {
+  helpers.audit(options, [
+    // These 3 establish a baseline for comparison when we have used the
+    // context command
+    {
+      setup:    'ext',
+      check: {
+        input:  'ext',
+        hints:     ' -> context',
+        markup: 'III',
+        message: '',
+        predictions: [ 'context', 'tsn ext', 'tsn exte', 'tsn exten', 'tsn extend' ],
+        unassigned: [ ],
+      }
+    },
+    {
+      setup:    'ext test',
+      check: {
+        input:  'ext test',
+        hints:          '',
+        markup: 'IIIVEEEE',
+        status: 'ERROR',
+        message: 'Too many arguments',
+        unassigned: [ ' test' ],
+      }
+    },
+    {
+      setup:    'tsn',
+      check: {
+        input:  'tsn',
+        hints:     '',
+        markup: 'III',
+        cursor: 3,
+        current: '__command',
+        status: 'ERROR',
+        predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+        args: {
+          command: { name: 'tsn' },
+        }
+      }
+    }
+  ]);
+};
+
+exports.testContext = function(options) {
+  helpers.audit(options, [
+    // Use the 'tsn' context
+    {
+      setup:    'context tsn',
+      check: {
+        input:  'context tsn',
+        hints:             '',
+        markup: 'VVVVVVVVVVV',
+        message: '',
+        predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+        args: {
+          command: { name: 'context' },
+          prefix: {
+            value: mockCommands.commands.tsn,
+            status: 'VALID',
+            message: ''
+          },
+        }
+      },
+      exec: {
+        output: 'Using tsn as a command prefix',
+        completed: true,
+      }
+    },
+    // For comparison with earlier
+    {
+      setup:    'ext',
+      check: {
+        input:  'ext',
+        hints:     ' <text>',
+        markup: 'VVV',
+        predictions: [ 'tsn ext', 'tsn exte', 'tsn exten', 'tsn extend' ],
+        args: {
+          command: { name: 'tsn ext' },
+          text: {
+            value: undefined,
+            arg: '',
+            status: 'INCOMPLETE',
+            message: ''
+          },
+        }
+      }
+    },
+    {
+      setup:    'ext test',
+      check: {
+        input:  'ext test',
+        hints:          '',
+        markup: 'VVVVVVVV',
+        args: {
+          command: { name: 'tsn ext' },
+          text: {
+            value: 'test',
+            arg: ' test',
+            status: 'VALID',
+            message: ''
+          },
+        }
+      },
+      exec: {
+        output: 'Exec: tsnExt text=test',
+        completed: true,
+      }
+    },
+    {
+      setup:    'tsn',
+      check: {
+        input:  'tsn',
+        hints:     '',
+        markup: 'III',
+        message: '',
+        predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+        args: {
+          command: { name: 'tsn' },
+        }
+      }
+    },
+    // Does it actually work?
+    {
+      setup:    'tsb true',
+      check: {
+        input:  'tsb true',
+        hints:          '',
+        markup: 'VVVVVVVV',
+        options: [ 'true' ],
+        message: '',
+        predictions: [ 'true' ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'tsb' },
+          toggle: { value: true, arg: ' true', status: 'VALID', message: '' },
+        }
+      }
+    },
+    {
+      // Bug 866710 - GCLI should allow argument merging for non-string parameters
+      setup: 'context tsn ext',
+      skip: true
+    },
+    {
+      setup:    'context "tsn ext"',
+      check: {
+        input:  'context "tsn ext"',
+        hints:                   '',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        message: '',
+        predictions: [ ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'context' },
+          prefix: {
+            value: mockCommands.commands.tsnExt,
+            status: 'VALID',
+            message: ''
+          }
+        }
+      },
+      exec: {
+        output: 'Error: Can\'t use \'tsn ext\' as a prefix because it is not a parent command.',
+        completed: true,
+        error: true
+      }
+    },
+    /*
+    {
+      setup:    'context "tsn deep"',
+      check: {
+        input:  'context "tsn deep"',
+        hints:                    '',
+        markup: 'VVVVVVVVVVVVVVVVVV',
+        status: 'ERROR',
+        message: '',
+        predictions: [ 'tsn deep' ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'context' },
+          prefix: {
+            value: mockCommands.commands.tsnDeep,
+            status: 'VALID',
+            message: ''
+          }
+        }
+      },
+      exec: {
+        output: '',
+        completed: true,
+      }
+    },
+    */
+    {
+      setup:    'context',
+      check: {
+        input:  'context',
+        hints:         ' [prefix]',
+        markup: 'VVVVVVV',
+        status: 'VALID',
+        unassigned: [ ],
+        args: {
+          command: { name: 'context' },
+          prefix: { value: undefined, arg: '', status: 'VALID', message: '' },
+        }
+      },
+      exec: {
+        output: 'Command prefix is unset',
+        completed: true,
+        type: 'string',
+        error: false
+      }
+    }
+  ]);
+};
+
+
+// });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_fail.js
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFail.js</p>";
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URI, function(options) {
+    return helpers.runTests(options, exports);
+  }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var cli = require('gcli/cli');
+
+var origLogErrors = undefined;
+
+exports.setup = function(options) {
+  mockCommands.setup();
+
+  origLogErrors = cli.logErrors;
+  cli.logErrors = false;
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+
+  cli.logErrors = origLogErrors;
+  origLogErrors = undefined;
+};
+
+exports.testBasic = function(options) {
+  return helpers.audit(options, [
+    {
+      setup: 'tsfail reject',
+      exec: {
+        completed: false,
+        output: 'rejected promise',
+        type: 'error',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail rejecttyped',
+      exec: {
+        completed: false,
+        output: '54',
+        type: 'number',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail throwerror',
+      exec: {
+        completed: true,
+        output: 'Error: thrown error',
+        type: 'error',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail throwstring',
+      exec: {
+        completed: true,
+        output: 'thrown string',
+        type: 'error',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail noerror',
+      exec: {
+        completed: true,
+        output: 'no error',
+        type: 'string',
+        error: false
+      }
+    }
+  ]);
+};
+
+
+// });
--- a/browser/devtools/commandline/test/browser_gcli_inputter.js
+++ b/browser/devtools/commandline/test/browser_gcli_inputter.js
@@ -35,48 +35,40 @@ function test() {
 
 'use strict';
 
 var KeyEvent = require('util/util').KeyEvent;
 // var assert = require('test/assert');
 // var mockCommands = require('gclitest/mockCommands');
 
 var latestEvent = undefined;
-var latestOutput = undefined;
 var latestData = undefined;
 
 var outputted = function(ev) {
-  function updateData() {
-    latestData = latestOutput.data;
-  }
+  latestEvent = ev;
 
-  if (latestOutput != null) {
-    ev.output.onChange.remove(updateData);
-  }
-
-  latestEvent = ev;
-  latestOutput = ev.output;
-
-  ev.output.onChange.add(updateData);
+  ev.output.promise.then(function() {
+    latestData = ev.output.data;
+    ev.output.onClose();
+  });
 };
 
 
 exports.setup = function(options) {
   mockCommands.setup();
   options.display.requisition.commandOutputManager.onOutput.add(outputted);
 };
 
 exports.shutdown = function(options) {
   mockCommands.shutdown();
   options.display.requisition.commandOutputManager.onOutput.remove(outputted);
 };
 
 exports.testOutput = function(options) {
   latestEvent = undefined;
-  latestOutput = undefined;
   latestData = undefined;
 
   var inputter = options.display.inputter;
   var focusManager = options.display.focusManager;
 
   inputter.setInput('tss');
 
   var ev0 = { keyCode: KeyEvent.DOM_VK_RETURN };
@@ -98,18 +90,16 @@ exports.testOutput = function(options) {
     var ev2 = { keyCode: KeyEvent.DOM_VK_F1 };
     return inputter.handleKeyUp(ev2).then(function() {
       assert.ok(!focusManager._recentOutput, 'no recent output happened post F1');
       assert.ok(focusManager._helpRequested, 'F1 = help');
 
       var ev3 = { keyCode: KeyEvent.DOM_VK_ESCAPE };
       return inputter.handleKeyUp(ev3).then(function() {
         assert.ok(!focusManager._helpRequested, 'ESCAPE = anti help');
-
-        latestOutput.onClose();
       });
     });
 
   });
 };
 
 
 // });
--- a/browser/devtools/commandline/test/browser_gcli_resource.js
+++ b/browser/devtools/commandline/test/browser_gcli_resource.js
@@ -56,87 +56,87 @@ exports.shutdown = function(options) {
 };
 
 exports.testAllPredictions1 = function(options) {
   if (options.isFirefox || options.isJsdom) {
     assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
     return;
   }
 
-  var resource = types.getType('resource');
+  var resource = types.createType('resource');
   return resource.getLookup().then(function(opts) {
     assert.ok(opts.length > 1, 'have all resources');
 
     return util.promiseEach(opts, function(prediction) {
       return checkPrediction(resource, prediction);
     });
   });
 };
 
 exports.testScriptPredictions = function(options) {
   if (options.isFirefox || options.isJsdom) {
     assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
     return;
   }
 
-  var resource = types.getType({ name: 'resource', include: 'text/javascript' });
+  var resource = types.createType({ name: 'resource', include: 'text/javascript' });
   return resource.getLookup().then(function(opts) {
     assert.ok(opts.length > 1, 'have js resources');
 
     return util.promiseEach(opts, function(prediction) {
       return checkPrediction(resource, prediction);
     });
   });
 };
 
 exports.testStylePredictions = function(options) {
   if (options.isFirefox || options.isJsdom) {
     assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
     return;
   }
 
-  var resource = types.getType({ name: 'resource', include: 'text/css' });
+  var resource = types.createType({ name: 'resource', include: 'text/css' });
   return resource.getLookup().then(function(opts) {
     assert.ok(opts.length >= 1, 'have css resources');
 
     return util.promiseEach(opts, function(prediction) {
       return checkPrediction(resource, prediction);
     });
   });
 };
 
 exports.testAllPredictions2 = function(options) {
   if (options.isJsdom) {
     assert.log('Skipping checks due to jsdom document.stylsheets support.');
     return;
   }
 
-  var scriptRes = types.getType({ name: 'resource', include: 'text/javascript' });
+  var scriptRes = types.createType({ name: 'resource', include: 'text/javascript' });
   return scriptRes.getLookup().then(function(scriptOptions) {
-    var styleRes = types.getType({ name: 'resource', include: 'text/css' });
+    var styleRes = types.createType({ name: 'resource', include: 'text/css' });
     return styleRes.getLookup().then(function(styleOptions) {
-      var allRes = types.getType({ name: 'resource' });
+      var allRes = types.createType({ name: 'resource' });
       return allRes.getLookup().then(function(allOptions) {
         assert.is(scriptOptions.length + styleOptions.length,
                   allOptions.length,
                   'split');
       });
     });
   });
 };
 
 exports.testAllPredictions3 = function(options) {
   if (options.isJsdom) {
     assert.log('Skipping checks due to jsdom document.stylsheets support.');
     return;
   }
 
-  var res1 = types.getType({ name: 'resource' });
+  var res1 = types.createType({ name: 'resource' });
   return res1.getLookup().then(function(options1) {
-    var res2 = types.getType('resource');
+    var res2 = types.createType('resource');
     return res2.getLookup().then(function(options2) {
       assert.is(options1.length, options2.length, 'type spec');
     });
   });
 };
 
 function checkPrediction(res, prediction) {
   var name = prediction.name;
--- a/browser/devtools/commandline/test/browser_gcli_split.js
+++ b/browser/devtools/commandline/test/browser_gcli_split.js
@@ -31,68 +31,69 @@ function test() {
   }).then(finish);
 }
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var assert = require('test/assert');
+var cli = require('gcli/cli');
 var Requisition = require('gcli/cli').Requisition;
 var canon = require('gcli/canon');
 // var mockCommands = require('gclitest/mockCommands');
 
 exports.setup = function(options) {
   mockCommands.setup();
 };
 
 exports.shutdown = function(options) {
   mockCommands.shutdown();
 };
 
 
 exports.testSplitSimple = function(options) {
   var args;
-  var requ = new Requisition();
+  var requisition = new Requisition();
 
-  args = requ._tokenize('s');
-  requ._split(args);
+  args = cli.tokenize('s');
+  requisition._split(args);
   assert.is(0, args.length);
-  assert.is('s', requ.commandAssignment.arg.text);
+  assert.is('s', requisition.commandAssignment.arg.text);
 };
 
 exports.testFlatCommand = function(options) {
   var args;
-  var requ = new Requisition();
+  var requisition = new Requisition();
 
-  args = requ._tokenize('tsv');
-  requ._split(args);
+  args = cli.tokenize('tsv');
+  requisition._split(args);
   assert.is(0, args.length);
-  assert.is('tsv', requ.commandAssignment.value.name);
+  assert.is('tsv', requisition.commandAssignment.value.name);
 
-  args = requ._tokenize('tsv a b');
-  requ._split(args);
-  assert.is('tsv', requ.commandAssignment.value.name);
+  args = cli.tokenize('tsv a b');
+  requisition._split(args);
+  assert.is('tsv', requisition.commandAssignment.value.name);
   assert.is(2, args.length);
   assert.is('a', args[0].text);
   assert.is('b', args[1].text);
 };
 
 exports.testJavascript = function(options) {
   if (!canon.getCommand('{')) {
     assert.log('Skipping testJavascript because { is not registered');
     return;
   }
 
   var args;
-  var requ = new Requisition();
+  var requisition = new Requisition();
 
-  args = requ._tokenize('{');
-  requ._split(args);
+  args = cli.tokenize('{');
+  requisition._split(args);
   assert.is(1, args.length);
   assert.is('', args[0].text);
-  assert.is('', requ.commandAssignment.arg.text);
-  assert.is('{', requ.commandAssignment.value.name);
+  assert.is('', requisition.commandAssignment.arg.text);
+  assert.is('{', requisition.commandAssignment.value.name);
 };
 
 // BUG 663081 - add tests for sub commands
 
 // });
--- a/browser/devtools/commandline/test/browser_gcli_tokenize.js
+++ b/browser/devtools/commandline/test/browser_gcli_tokenize.js
@@ -31,172 +31,167 @@ function test() {
   }).then(finish);
 }
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var assert = require('test/assert');
-var Requisition = require('gcli/cli').Requisition;
+var cli = require('gcli/cli');
 
 exports.testBlanks = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('');
+  args = cli.tokenize('');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
 
-  args = requ._tokenize(' ');
+  args = cli.tokenize(' ');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is(' ', args[0].prefix);
   assert.is('', args[0].suffix);
 };
 
 exports.testTokSimple = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('s');
+  args = cli.tokenize('s');
   assert.is(1, args.length);
   assert.is('s', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
-  args = requ._tokenize('s s');
+  args = cli.tokenize('s s');
   assert.is(2, args.length);
   assert.is('s', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
   assert.is('s', args[1].text);
   assert.is(' ', args[1].prefix);
   assert.is('', args[1].suffix);
   assert.is('Argument', args[1].type);
 };
 
 exports.testJavascript = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('{x}');
+  args = cli.tokenize('{x}');
   assert.is(1, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{ x }');
+  args = cli.tokenize('{ x }');
   assert.is(1, args.length);
   assert.is('x', args[0].text);
   assert.is('{ ', args[0].prefix);
   assert.is(' }', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{x} {y}');
+  args = cli.tokenize('{x} {y}');
   assert.is(2, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
   assert.is('y', args[1].text);
   assert.is(' {', args[1].prefix);
   assert.is('}', args[1].suffix);
   assert.is('ScriptArgument', args[1].type);
 
-  args = requ._tokenize('{x}{y}');
+  args = cli.tokenize('{x}{y}');
   assert.is(2, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
   assert.is('y', args[1].text);
   assert.is('{', args[1].prefix);
   assert.is('}', args[1].suffix);
   assert.is('ScriptArgument', args[1].type);
 
-  args = requ._tokenize('{');
+  args = cli.tokenize('{');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{ ');
+  args = cli.tokenize('{ ');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is('{ ', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{x');
+  args = cli.tokenize('{x');
   assert.is(1, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 };
 
 exports.testRegularNesting = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('{"x"}');
+  args = cli.tokenize('{"x"}');
   assert.is(1, args.length);
   assert.is('"x"', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{\'x\'}');
+  args = cli.tokenize('{\'x\'}');
   assert.is(1, args.length);
   assert.is('\'x\'', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('"{x}"');
+  args = cli.tokenize('"{x}"');
   assert.is(1, args.length);
   assert.is('{x}', args[0].text);
   assert.is('"', args[0].prefix);
   assert.is('"', args[0].suffix);
   assert.is('Argument', args[0].type);
 
-  args = requ._tokenize('\'{x}\'');
+  args = cli.tokenize('\'{x}\'');
   assert.is(1, args.length);
   assert.is('{x}', args[0].text);
   assert.is('\'', args[0].prefix);
   assert.is('\'', args[0].suffix);
   assert.is('Argument', args[0].type);
 };
 
 exports.testDeepNesting = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('{{}}');
+  args = cli.tokenize('{{}}');
   assert.is(1, args.length);
   assert.is('{}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{{x} {y}}');
+  args = cli.tokenize('{{x} {y}}');
   assert.is(1, args.length);
   assert.is('{x} {y}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{{w} {{{x}}}} {y} {{{z}}}');
+  args = cli.tokenize('{{w} {{{x}}}} {y} {{{z}}}');
 
   assert.is(3, args.length);
 
   assert.is('{w} {{{x}}}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
@@ -205,65 +200,63 @@ exports.testDeepNesting = function(optio
   assert.is('}', args[1].suffix);
   assert.is('ScriptArgument', args[1].type);
 
   assert.is('{{z}}', args[2].text);
   assert.is(' {', args[2].prefix);
   assert.is('}', args[2].suffix);
   assert.is('ScriptArgument', args[2].type);
 
-  args = requ._tokenize('{{w} {{{x}}} {y} {{{z}}}');
+  args = cli.tokenize('{{w} {{{x}}} {y} {{{z}}}');
 
   assert.is(1, args.length);
 
   assert.is('{w} {{{x}}} {y} {{{z}}}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 };
 
 exports.testStrangeNesting = function(options) {
   var args;
-  var requ = new Requisition();
 
   // Note: When we get real JS parsing this should break
-  args = requ._tokenize('{"x}"}');
+  args = cli.tokenize('{"x}"}');
 
   assert.is(2, args.length);
 
   assert.is('"x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
   assert.is('}', args[1].text);
   assert.is('"', args[1].prefix);
   assert.is('', args[1].suffix);
   assert.is('Argument', args[1].type);
 };
 
 exports.testComplex = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize(' 1234  \'12 34\'');
+  args = cli.tokenize(' 1234  \'12 34\'');
 
   assert.is(2, args.length);
 
   assert.is('1234', args[0].text);
   assert.is(' ', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
   assert.is('12 34', args[1].text);
   assert.is('  \'', args[1].prefix);
   assert.is('\'', args[1].suffix);
   assert.is('Argument', args[1].type);
 
-  args = requ._tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
+  args = cli.tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
 
   assert.is(3, args.length);
 
   assert.is('12\'34', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
@@ -275,19 +268,18 @@ exports.testComplex = function(options) 
   assert.is('\\', args[2].text);
   assert.is(' ', args[2].prefix);
   assert.is('', args[2].suffix);
   assert.is('Argument', args[2].type);
 };
 
 exports.testPathological = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
+  args = cli.tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
 
   assert.is(4, args.length);
 
   assert.is('a b', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
--- a/browser/devtools/commandline/test/browser_gcli_types.js
+++ b/browser/devtools/commandline/test/browser_gcli_types.js
@@ -44,25 +44,32 @@ function forEachType(options, typeSpec, 
     typeSpec.requisition = options.display.requisition;
 
     // Provide some basic defaults to help selection/delegate/array work
     if (name === 'selection') {
       typeSpec.data = [ 'a', 'b' ];
     }
     else if (name === 'delegate') {
       typeSpec.delegateType = function() {
-        return types.getType('string');
+        return types.createType('string');
       };
     }
     else if (name === 'array') {
       typeSpec.subtype = 'string';
     }
 
-    var type = types.getType(typeSpec);
+    var type = types.createType(typeSpec);
     callback(type);
+
+    // Clean up
+    delete typeSpec.name;
+    delete typeSpec.requisition;
+    delete typeSpec.data;
+    delete typeSpec.delegateType;
+    delete typeSpec.subtype;
   });
 }
 
 exports.testDefault = function(options) {
   if (options.isJsdom) {
     assert.log('Skipping tests due to issues with resource type.');
     return;
   }
--- a/browser/devtools/commandline/test/helpers.js
+++ b/browser/devtools/commandline/test/helpers.js
@@ -16,21 +16,20 @@
 //   };
 // Now GCLI will emit output on every keypress that both explains the state
 // of GCLI and can be run as a test case.
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
-Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/commandline/test/mockCommands.js
+++ b/browser/devtools/commandline/test/mockCommands.js
@@ -18,270 +18,193 @@
 
 // <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
 // DO NOT EDIT IT DIRECTLY
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 let { require: require, define: define } = Cu.import("resource://gre/modules/devtools/Require.jsm", {});
-Cu.import("resource:///modules/devtools/gcli.jsm", {});
+Cu.import("resource://gre/modules/devtools/gcli.jsm", {});
 
 // <INJECTED SOURCE:END>
 
 var mockCommands = {};
 
+// We use an alias for exports here because this module is used in Firefox
+// mochitests where we don't have define/require
+
+'use strict';
+
 var util = require('util/util');
 var canon = require('gcli/canon');
-
 var types = require('gcli/types');
-var SelectionType = require('gcli/types/selection').SelectionType;
-var DelegateType = require('gcli/types/basic').DelegateType;
-
-
-/**
- * Registration and de-registration.
- */
-mockCommands.setup = function(opts) {
-  // setup/shutdown needs to register/unregister types, however that means we
-  // need to re-initialize mockCommands.option1 and mockCommands.option2 with
-  // the actual types
-  mockCommands.option1.type = types.getType('string');
-  mockCommands.option2.type = types.getType('number');
-
-  types.registerType(mockCommands.optionType);
-  types.registerType(mockCommands.optionValue);
-
-  canon.addCommand(mockCommands.tsv);
-  canon.addCommand(mockCommands.tsr);
-  canon.addCommand(mockCommands.tso);
-  canon.addCommand(mockCommands.tse);
-  canon.addCommand(mockCommands.tsj);
-  canon.addCommand(mockCommands.tsb);
-  canon.addCommand(mockCommands.tss);
-  canon.addCommand(mockCommands.tsu);
-  canon.addCommand(mockCommands.tsf);
-  canon.addCommand(mockCommands.tsn);
-  canon.addCommand(mockCommands.tsnDif);
-  canon.addCommand(mockCommands.tsnExt);
-  canon.addCommand(mockCommands.tsnExte);
-  canon.addCommand(mockCommands.tsnExten);
-  canon.addCommand(mockCommands.tsnExtend);
-  canon.addCommand(mockCommands.tsnDeep);
-  canon.addCommand(mockCommands.tsnDeepDown);
-  canon.addCommand(mockCommands.tsnDeepDownNested);
-  canon.addCommand(mockCommands.tsnDeepDownNestedCmd);
-  canon.addCommand(mockCommands.tselarr);
-  canon.addCommand(mockCommands.tsm);
-  canon.addCommand(mockCommands.tsg);
-  canon.addCommand(mockCommands.tshidden);
-  canon.addCommand(mockCommands.tscook);
-  canon.addCommand(mockCommands.tslong);
-};
 
-mockCommands.shutdown = function(opts) {
-  canon.removeCommand(mockCommands.tsv);
-  canon.removeCommand(mockCommands.tsr);
-  canon.removeCommand(mockCommands.tso);
-  canon.removeCommand(mockCommands.tse);
-  canon.removeCommand(mockCommands.tsj);
-  canon.removeCommand(mockCommands.tsb);
-  canon.removeCommand(mockCommands.tss);
-  canon.removeCommand(mockCommands.tsu);
-  canon.removeCommand(mockCommands.tsf);
-  canon.removeCommand(mockCommands.tsn);
-  canon.removeCommand(mockCommands.tsnDif);
-  canon.removeCommand(mockCommands.tsnExt);
-  canon.removeCommand(mockCommands.tsnExte);
-  canon.removeCommand(mockCommands.tsnExten);
-  canon.removeCommand(mockCommands.tsnExtend);
-  canon.removeCommand(mockCommands.tsnDeep);
-  canon.removeCommand(mockCommands.tsnDeepDown);
-  canon.removeCommand(mockCommands.tsnDeepDownNested);
-  canon.removeCommand(mockCommands.tsnDeepDownNestedCmd);
-  canon.removeCommand(mockCommands.tselarr);
-  canon.removeCommand(mockCommands.tsm);
-  canon.removeCommand(mockCommands.tsg);
-  canon.removeCommand(mockCommands.tshidden);
-  canon.removeCommand(mockCommands.tscook);
-  canon.removeCommand(mockCommands.tslong);
+mockCommands.option1 = { };
+mockCommands.option2 = { };
+mockCommands.option3 = { };
 
-  types.deregisterType(mockCommands.optionType);
-  types.deregisterType(mockCommands.optionValue);
-};
-
-
-mockCommands.option1 = { type: types.getType('string') };
-mockCommands.option2 = { type: types.getType('number') };
-mockCommands.option3 = { type: types.getType({
-  name: 'selection',
-  lookup: [
-    { name: 'one', value: 1 },
-    { name: 'two', value: 2 },
-    { name: 'three', value: 3 }
-  ]
-})};
-
-mockCommands.optionType = new SelectionType({
+mockCommands.optionType = {
   name: 'optionType',
+  parent: 'selection',
   lookup: [
     { name: 'option1', value: mockCommands.option1 },
     { name: 'option2', value: mockCommands.option2 },
     { name: 'option3', value: mockCommands.option3 }
   ]
-});
+};
 
-mockCommands.optionValue = new DelegateType({
+mockCommands.optionValue = {
   name: 'optionValue',
-  delegateType: function(context) {
-    if (context != null) {
-      var option = context.getArgsObject().optionType;
+  parent: 'delegate',
+  delegateType: function(executionContext) {
+    if (executionContext != null) {
+      var option = executionContext.getArgsObject().optionType;
       if (option != null) {
         return option.type;
       }
     }
-    return types.getType('blank');
+    return types.createType('blank');
   }
-});
+};
 
 mockCommands.onCommandExec = util.createEvent('commands.onCommandExec');
 
 function createExec(name) {
-  return function(args, context) {
+  return function(args, executionContext) {
     var data = {
-      command: mockCommands[name],
       args: args,
-      context: context
+      context: executionContext
     };
     mockCommands.onCommandExec(data);
     var argsOut = Object.keys(args).map(function(key) {
       return key + '=' + args[key];
     }).join(', ');
     return 'Exec: ' + name + ' ' + argsOut;
   };
 }
 
-mockCommands.tsv = {
+var tsv = {
   name: 'tsv',
   params: [
     { name: 'optionType', type: 'optionType' },
     { name: 'optionValue', type: 'optionValue' }
   ],
   exec: createExec('tsv')
 };
 
-mockCommands.tsr = {
+var tsr = {
   name: 'tsr',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsr')
 };
 
-mockCommands.tso = {
+var tso = {
   name: 'tso',
   params: [ { name: 'text', type: 'string', defaultValue: null } ],
   exec: createExec('tso')
 };
 
-mockCommands.tse = {
+var tse = {
   name: 'tse',
   params: [
     { name: 'node', type: 'node' },
     {
       group: 'options',
       params: [
         { name: 'nodes', type: { name: 'nodelist' } },
         { name: 'nodes2', type: { name: 'nodelist', allowEmpty: true } }
       ]
     }
   ],
   exec: createExec('tse')
 };
 
-mockCommands.tsj = {
+var tsj = {
   name: 'tsj',
   params: [ { name: 'javascript', type: 'javascript' } ],
   exec: createExec('tsj')
 };
 
-mockCommands.tsb = {
+var tsb = {
   name: 'tsb',
   params: [ { name: 'toggle', type: 'boolean' } ],
   exec: createExec('tsb')
 };
 
-mockCommands.tss = {
+var tss = {
   name: 'tss',
   exec: createExec('tss')
 };
 
-mockCommands.tsu = {
+var tsu = {
   name: 'tsu',
   params: [ { name: 'num', type: { name: 'number', max: 10, min: -5, step: 3 } } ],
   exec: createExec('tsu')
 };
 
-mockCommands.tsf = {
+var tsf = {
   name: 'tsf',
   params: [ { name: 'num', type: { name: 'number', allowFloat: true, max: 11.5, min: -6.5, step: 1.5 } } ],
   exec: createExec('tsf')
 };
 
-mockCommands.tsn = {
+var tsn = {
   name: 'tsn'
 };
 
-mockCommands.tsnDif = {
+var tsnDif = {
   name: 'tsn dif',
   description: 'tsn dif',
   params: [ { name: 'text', type: 'string', description: 'tsn dif text' } ],
   exec: createExec('tsnDif')
 };
 
-mockCommands.tsnExt = {
+var tsnExt = {
   name: 'tsn ext',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExt')
 };
 
-mockCommands.tsnExte = {
+var tsnExte = {
   name: 'tsn exte',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExte')
 };
 
-mockCommands.tsnExten = {
+var tsnExten = {
   name: 'tsn exten',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExten')
 };
 
-mockCommands.tsnExtend = {
+var tsnExtend = {
   name: 'tsn extend',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExtend')
 };
 
-mockCommands.tsnDeep = {
+var tsnDeep = {
   name: 'tsn deep'
 };
 
-mockCommands.tsnDeepDown = {
+var tsnDeepDown = {
   name: 'tsn deep down'
 };
 
-mockCommands.tsnDeepDownNested = {
+var tsnDeepDownNested = {
   name: 'tsn deep down nested'
 };
 
-mockCommands.tsnDeepDownNestedCmd = {
+var tsnDeepDownNestedCmd = {
   name: 'tsn deep down nested cmd',
   exec: createExec('tsnDeepDownNestedCmd')
 };
 
-mockCommands.tshidden = {
+var tshidden = {
   name: 'tshidden',
   hidden: true,
   params: [
     {
       group: 'Options',
       params: [
         {
           name: 'visible',
@@ -303,37 +226,37 @@ mockCommands.tshidden = {
           hidden: true
         }
       ]
     }
   ],
   exec: createExec('tshidden')
 };
 
-mockCommands.tselarr = {
+var tselarr = {
   name: 'tselarr',
   params: [
     { name: 'num', type: { name: 'selection', data: [ '1', '2', '3' ] } },
     { name: 'arr', type: { name: 'array', subtype: 'string' } }
   ],
   exec: createExec('tselarr')
 };
 
-mockCommands.tsm = {
+var tsm = {
   name: 'tsm',
   description: 'a 3-param test selection|string|number',
   params: [
     { name: 'abc', type: { name: 'selection', data: [ 'a', 'b', 'c' ] } },
     { name: 'txt', type: 'string' },
     { name: 'num', type: { name: 'number', max: 42, min: 0 } }
   ],
   exec: createExec('tsm')
 };
 
-mockCommands.tsg = {
+var tsg = {
   name: 'tsg',
   description: 'a param group test',
   params: [
     {
       name: 'solo',
       type: { name: 'selection', data: [ 'aaa', 'bbb', 'ccc' ] },
       description: 'solo param'
     },
@@ -366,17 +289,17 @@ mockCommands.tsg = {
       defaultValue: 42,
       description: 'num param',
       option: 'Second'
     }
   ],
   exec: createExec('tsg')
 };
 
-mockCommands.tscook = {
+var tscook = {
   name: 'tscook',
   description: 'param group test to catch problems with cookie command',
   params: [
     {
       name: 'key',
       type: 'string',
       description: 'tscookKeyDesc'
     },
@@ -406,20 +329,19 @@ mockCommands.tscook = {
           description: 'tscookSecureDesc'
         }
       ]
     }
   ],
   exec: createExec('tscook')
 };
 
-mockCommands.tslong = {
+var tslong = {
   name: 'tslong',
   description: 'long param tests to catch problems with the jsb command',
-  returnValue:'string',
   params: [
     {
       name: 'msg',
       type: 'string',
       description: 'msg Desc'
     },
     {
       group: "Options Desc",
@@ -468,10 +390,151 @@ mockCommands.tslong = {
           defaultValue: "collapse"
         }
       ]
     }
   ],
   exec: createExec('tslong')
 };
 
+var tsfail = {
+  name: 'tsfail',
+  description: 'test errors',
+  params: [
+    {
+      name: 'method',
+      type: {
+        name: 'selection',
+        data: [
+          'reject', 'rejecttyped',
+          'throwerror', 'throwstring', 'throwinpromise',
+          'noerror'
+        ]
+      }
+    }
+  ],
+  exec: function(args, context) {
+    if (args.method === 'reject') {
+      var deferred = context.defer();
+      setTimeout(function() {
+        deferred.reject('rejected promise');
+      }, 10);
+      return deferred.promise;
+    }
+
+    if (args.method === 'rejecttyped') {
+      var deferred = context.defer();
+      setTimeout(function() {
+        deferred.reject(context.typedData('number', 54));
+      }, 10);
+      return deferred.promise;
+    }
+
+    if (args.method === 'throwinpromise') {
+      var deferred = context.defer();
+      setTimeout(function() {
+        deferred.resolve('should be lost');
+      }, 10);
+      return deferred.promise.then(function() {
+        var t = null;
+        return t.foo;
+      });
+    }
+
+    if (args.method === 'throwerror') {
+      throw new Error('thrown error');
+    }
+
+    if (args.method === 'throwstring') {
+      throw 'thrown string';
+    }
+
+    return 'no error';
+  }
+};
+
+mockCommands.commands = {};
+
+/**
+ * Registration and de-registration.
+ */
+mockCommands.setup = function(opts) {
+  // setup/shutdown needs to register/unregister types, however that means we
+  // need to re-initialize mockCommands.option1 and mockCommands.option2 with
+  // the actual types
+  mockCommands.option1.type = types.createType('string');
+  mockCommands.option2.type = types.createType('number');
+  mockCommands.option3.type = types.createType({
+    name: 'selection',
+    lookup: [
+      { name: 'one', value: 1 },
+      { name: 'two', value: 2 },
+      { name: 'three', value: 3 }
+    ]
+  });
+
+  types.addType(mockCommands.optionType);
+  types.addType(mockCommands.optionValue);
+
+  mockCommands.commands.tsv = canon.addCommand(tsv);
+  mockCommands.commands.tsr = canon.addCommand(tsr);
+  mockCommands.commands.tso = canon.addCommand(tso);
+  mockCommands.commands.tse = canon.addCommand(tse);
+  mockCommands.commands.tsj = canon.addCommand(tsj);
+  mockCommands.commands.tsb = canon.addCommand(tsb);
+  mockCommands.commands.tss = canon.addCommand(tss);
+  mockCommands.commands.tsu = canon.addCommand(tsu);
+  mockCommands.commands.tsf = canon.addCommand(tsf);
+  mockCommands.commands.tsn = canon.addCommand(tsn);
+  mockCommands.commands.tsnDif = canon.addCommand(tsnDif);
+  mockCommands.commands.tsnExt = canon.addCommand(tsnExt);
+  mockCommands.commands.tsnExte = canon.addCommand(tsnExte);
+  mockCommands.commands.tsnExten = canon.addCommand(tsnExten);
+  mockCommands.commands.tsnExtend = canon.addCommand(tsnExtend);
+  mockCommands.commands.tsnDeep = canon.addCommand(tsnDeep);
+  mockCommands.commands.tsnDeepDown = canon.addCommand(tsnDeepDown);
+  mockCommands.commands.tsnDeepDownNested = canon.addCommand(tsnDeepDownNested);
+  mockCommands.commands.tsnDeepDownNestedCmd = canon.addCommand(tsnDeepDownNestedCmd);
+  mockCommands.commands.tselarr = canon.addCommand(tselarr);
+  mockCommands.commands.tsm = canon.addCommand(tsm);
+  mockCommands.commands.tsg = canon.addCommand(tsg);
+  mockCommands.commands.tshidden = canon.addCommand(tshidden);
+  mockCommands.commands.tscook = canon.addCommand(tscook);
+  mockCommands.commands.tslong = canon.addCommand(tslong);
+  mockCommands.commands.tsfail = canon.addCommand(tsfail);
+};
+
+mockCommands.shutdown = function(opts) {
+  canon.removeCommand(tsv);
+  canon.removeCommand(tsr);
+  canon.removeCommand(tso);
+  canon.removeCommand(tse);
+  canon.removeCommand(tsj);
+  canon.removeCommand(tsb);
+  canon.removeCommand(tss);
+  canon.removeCommand(tsu);
+  canon.removeCommand(tsf);
+  canon.removeCommand(tsn);
+  canon.removeCommand(tsnDif);
+  canon.removeCommand(tsnExt);
+  canon.removeCommand(tsnExte);
+  canon.removeCommand(tsnExten);
+  canon.removeCommand(tsnExtend);
+  canon.removeCommand(tsnDeep);
+  canon.removeCommand(tsnDeepDown);
+  canon.removeCommand(tsnDeepDownNested);
+  canon.removeCommand(tsnDeepDownNestedCmd);
+  canon.removeCommand(tselarr);
+  canon.removeCommand(tsm);
+  canon.removeCommand(tsg);
+  canon.removeCommand(tshidden);
+  canon.removeCommand(tscook);
+  canon.removeCommand(tslong);
+  canon.removeCommand(tsfail);
+
+  types.removeType(mockCommands.optionType);
+  types.removeType(mockCommands.optionValue);
+
+  mockCommands.commands = {};
+};
+
 
 // });
--- a/browser/devtools/debugger/CmdDebugger.jsm
+++ b/browser/devtools/debugger/CmdDebugger.jsm
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 this.EXPORTED_SYMBOLS = [ ];
 
-Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/devtools/Console.jsm");
 
@@ -152,17 +152,17 @@ gcli.addCommand({
       description: gcli.lookup("breakaddlineFileDesc")
     },
     {
       name: "line",
       type: { name: "number", min: 1, step: 10 },
       description: gcli.lookup("breakaddlineLineDesc")
     }
   ],
-  returnType: "html",
+  returnType: "string",
   exec: function(args, context) {
     args.type = "line";
 
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
       return gcli.lookup("debuggerStopped");
     }
     var deferred = context.defer();
@@ -196,17 +196,17 @@ gcli.addCommand({
           return dbg == null ?
               null :
               Object.keys(dbg.getAllBreakpoints()).length - 1;
         },
       },
       description: gcli.lookup("breakdelBreakidDesc")
     }
   ],
-  returnType: "html",
+  returnType: "string",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
       return gcli.lookup("debuggerStopped");
     }
 
     let breakpoints = dbg.getAllBreakpoints();
     let id = Object.keys(breakpoints)[args.breakid];
@@ -376,17 +376,17 @@ gcli.addCommand({
 
 /**
  * 'dbg list' command
  */
 gcli.addCommand({
   name: "dbg list",
   description: gcli.lookup("dbgListSourcesDesc"),
   params: [],
-  returnType: "html",
+  returnType: "dom",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     let doc = context.environment.chromeDocument;
     if (!dbg) {
       return gcli.lookup("debuggerClosed");
     }
     let sources = dbg._view.Sources.values;
     let div = createXHTMLElement(doc, "div");
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -792,17 +792,19 @@ StackFrames.prototype = {
     aScope._fetched = true;
     let env = aScope._sourceEnvironment;
 
     switch (env.type) {
       case "with":
       case "object":
         // Add nodes for every variable in scope.
         this.activeThread.pauseGrip(env.object).getPrototypeAndProperties(function(aResponse) {
-          this._insertScopeVariables(aResponse.ownProperties, aScope);
+          let { ownProperties, safeGetterValues } = aResponse;
+          this._mergeSafeGetterValues(ownProperties, safeGetterValues);
+          this._insertScopeVariables(ownProperties, aScope);
 
           // Signal that variables have been fetched.
           window.dispatchEvent(document, "Debugger:FetchedVariables");
           DebuggerView.Variables.commitHierarchy();
         }.bind(this));
         break;
       case "block":
       case "function":
@@ -898,19 +900,21 @@ StackFrames.prototype = {
     // Fetch the properties only once.
     if (aVar._fetched) {
       return;
     }
     aVar._fetched = true;
     let grip = aVar._sourceGrip;
 
     this.activeThread.pauseGrip(grip).getPrototypeAndProperties(function(aResponse) {
-      let { ownProperties, prototype } = aResponse;
+      let { ownProperties, prototype, safeGetterValues } = aResponse;
       let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
 
+      this._mergeSafeGetterValues(ownProperties, safeGetterValues);
+
       // Add all the variable properties.
       if (ownProperties) {
         aVar.addProperties(ownProperties, {
           // Not all variables need to force sorted properties.
           sorted: sortable,
           // Expansion handlers must be set after the properties are added.
           callback: this._addVarExpander
         });
@@ -928,16 +932,43 @@ StackFrames.prototype = {
 
       // Signal that properties have been fetched.
       window.dispatchEvent(document, "Debugger:FetchedProperties");
       DebuggerView.Variables.commitHierarchy();
     }.bind(this));
   },
 
   /**
+   * Merge the safe getter values descriptors into the "own properties" object
+   * that comes from a "prototypeAndProperties" response packet. This is needed
+   * for Variables View.
+   *
+   * @private
+   * @param object aOwnProperties
+   *        The |ownProperties| object that will get the new safe getter values.
+   * @param object aSafeGetterValues
+   *        The |safeGetterValues| object.
+   */
+  _mergeSafeGetterValues:
+  function SF__mergeSafeGetterValues(aOwnProperties, aSafeGetterValues) {
+    // Merge the safe getter values into one object such that we can use it
+    // in VariablesView.
+    for (let name of Object.keys(aSafeGetterValues)) {
+      if (name in aOwnProperties) {
+        aOwnProperties[name].getterValue = aSafeGetterValues[name].getterValue;
+        aOwnProperties[name].getterPrototypeLevel = aSafeGetterValues[name]
+                                                    .getterPrototypeLevel;
+      }
+      else {
+        aOwnProperties[name] = aSafeGetterValues[name];
+      }
+    }
+  },
+
+  /**
    * Adds the specified stack frame to the list.
    *
    * @param object aFrame
    *        The new frame to add.
    */
   _addFrame: function SF__addFrame(aFrame) {
     let depth = aFrame.depth;
     let { url, line } = aFrame.where;
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -401,17 +401,16 @@ create({ constructor: StackFramesView, p
   addFrame: function DVSF_addFrame(aFrameTitle, aSourceLocation, aLineNumber, aDepth) {
     // Create the element node and menu entry for the stack frame item.
     let frameView = this._createFrameView.apply(this, arguments);
     let menuEntry = this._createMenuEntry.apply(this, arguments);
 
     // Append a stack frame item to this container.
     let stackframeItem = this.push(frameView, {
       index: 0, /* specifies on which position should the item be appended */
-      relaxed: true, /* this container should allow dupes & degenerates */
       attachment: {
         popup: menuEntry,
         depth: aDepth
       },
       attributes: [
         ["contextmenu", "stackframesMenupopup"],
         ["tooltiptext", aSourceLocation]
       ],
@@ -941,17 +940,17 @@ FilterView.prototype = {
     }
     // Synchronize with the view's filtered sources container.
     DebuggerView.FilteredSources.syncFileSearch();
 
     // Hide all the groups with no visible children.
     view.node.hideEmptyGroups();
 
     // Ensure the currently selected item is visible.
-    view.node.ensureSelectionIsVisible(true);
+    view.node.ensureSelectionIsVisible({ withGroup: true });
 
     // Remember the previously searched file to avoid redundant filtering.
     this._prevSearchedFile = aFile;
   },
 
   /**
    * Performs a line search if necessary.
    * (Jump to lines in the currently visible source).
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-09.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-09.js
@@ -51,20 +51,23 @@ function testFrameParameters()
         "Should have three frames.");
 
       is(globalNodes[1].querySelector(".name").getAttribute("value"), "SpecialPowers",
         "Should have the right property name for |SpecialPowers|.");
 
       is(globalNodes[1].querySelector(".value").getAttribute("value"), "[object Object]",
         "Should have the right property value for |SpecialPowers|.");
 
-      is(globalNodes[3].querySelector(".name").getAttribute("value"), "document",
+      let globalScopeObject = gDebugger.DebuggerView.Variables.getScopeForNode(globalScope);
+      let documentNode = globalScopeObject.get("document");
+
+      is(documentNode.target.querySelector(".name").getAttribute("value"), "document",
         "Should have the right property name for |document|.");
 
-      is(globalNodes[3].querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
+      is(documentNode.target.querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
         "Should have the right property value for |document|.");
 
       let len = globalNodes.length - 1;
       is(globalNodes[len].querySelector(".name").getAttribute("value"), "window",
         "Should have the right property name for |window|.");
 
       is(globalNodes[len].querySelector(".value").getAttribute("value"), "[object Window]",
         "Should have the right property value for |window|.");
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-10.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-10.js
@@ -56,20 +56,23 @@ function testWithFrame()
       is(scopes.childNodes.length, 5, "Should have 5 variable scopes.");
 
       is(innerNodes[1].querySelector(".name").getAttribute("value"), "one",
         "Should have the right property name for |one|.");
 
       is(innerNodes[1].querySelector(".value").getAttribute("value"), "1",
         "Should have the right property value for |one|.");
 
-      is(globalNodes[3].querySelector(".name").getAttribute("value"), "document",
+      let globalScopeObject = gDebugger.DebuggerView.Variables.getScopeForNode(globalScope);
+      let documentNode = globalScopeObject.get("document");
+
+      is(documentNode.target.querySelector(".name").getAttribute("value"), "document",
         "Should have the right property name for |document|.");
 
-      is(globalNodes[3].querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
+      is(documentNode.target.querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
         "Should have the right property value for |document|.");
 
       let len = globalNodes.length - 1;
       is(globalNodes[len].querySelector(".name").getAttribute("value"), "window",
         "Should have the right property name for |window|.");
 
       is(globalNodes[len].querySelector(".value").getAttribute("value"), "[object Window]",
         "Should have the right property value for |window|.");
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-11.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-11.js
@@ -56,25 +56,27 @@ function testFrameParameters()
         "Should have the right property value for |button|.");
 
       is(anonymousNodes[2].querySelector(".name").getAttribute("value"), "buttonAsProto",
         "Should have the right property name for |buttonAsProto|.");
 
       is(anonymousNodes[2].querySelector(".value").getAttribute("value"), "[object Object]",
         "Should have the right property value for |buttonAsProto|.");
 
-      is(globalNodes[3].querySelector(".name").getAttribute("value"), "document",
+      let globalScopeObject = gVars.getScopeForNode(globalScope);
+      let documentNode = globalScopeObject.get("document");
+
+      is(documentNode.target.querySelector(".name").getAttribute("value"), "document",
         "Should have the right property name for |document|.");
 
-      is(globalNodes[3].querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
+      is(documentNode.target.querySelector(".value").getAttribute("value"), "[object HTMLDocument]",
         "Should have the right property value for |document|.");
 
       let buttonNode = gVars.getItemForNode(anonymousNodes[1]);
       let buttonAsProtoNode = gVars.getItemForNode(anonymousNodes[2]);
-      let documentNode = gVars.getItemForNode(globalNodes[3]);
 
       is(buttonNode.expanded, false,
         "The buttonNode should not be expanded at this point.");
       is(buttonAsProtoNode.expanded, false,
         "The buttonAsProtoNode should not be expanded at this point.");
       is(documentNode.expanded, false,
         "The documentNode should not be expanded at this point.");
 
@@ -126,133 +128,90 @@ function testFrameParameters()
 
         is(documentNode.get("__proto__").target.querySelector(".name")
            .getAttribute("value"), "__proto__",
           "Should have the right property name for '__proto__' in documentNode.");
         ok(documentNode.get("__proto__").target.querySelector(".value")
            .getAttribute("value").search(/object/) != -1,
           "'__proto__' in documentNode should be an object.");
 
-        let buttonProtoNode = buttonNode.get("__proto__");
+        // Now the main course: make sure that the native getters for WebIDL
+        // attributes have been called and a value has been returned.
+        is(buttonNode.get("type").target.querySelector(".name")
+           .getAttribute("value"), "type",
+          "Should have the right property name for 'type' in buttonProtoNode.");
+        is(buttonNode.get("type").target.querySelector(".value")
+           .getAttribute("value"), '"submit"',
+          "'type' in buttonProtoNode should have the right value.");
+        is(buttonNode.get("formMethod").target.querySelector(".name")
+           .getAttribute("value"), "formMethod",
+          "Should have the right property name for 'formMethod' in buttonProtoNode.");
+        is(buttonNode.get("formMethod").target.querySelector(".value")
+           .getAttribute("value"), '""',
+          "'formMethod' in buttonProtoNode should have the right value.");
+
+        is(documentNode.get("domain").target.querySelector(".name")
+           .getAttribute("value"), "domain",
+          "Should have the right property name for 'domain' in documentProtoNode.");
+        is(documentNode.get("domain").target.querySelector(".value")
+           .getAttribute("value"), '"example.com"',
+          "'domain' in documentProtoNode should have the right value.");
+        is(documentNode.get("cookie").target.querySelector(".name")
+           .getAttribute("value"), "cookie",
+          "Should have the right property name for 'cookie' in documentProtoNode.");
+        is(documentNode.get("cookie").target.querySelector(".value")
+           .getAttribute("value"), '""',
+          "'cookie' in documentProtoNode should have the right value.");
+
         let buttonAsProtoProtoNode = buttonAsProtoNode.get("__proto__");
-        let documentProtoNode = documentNode.get("__proto__");
 
-        is(buttonProtoNode.expanded, false,
-          "The buttonProtoNode should not be expanded at this point.");
         is(buttonAsProtoProtoNode.expanded, false,
           "The buttonAsProtoProtoNode should not be expanded at this point.");
-        is(documentProtoNode.expanded, false,
-          "The documentProtoNode should not be expanded at this point.");
 
         // Expand the prototypes of 'button', 'buttonAsProto' and 'document'
         // tree nodes. This causes their properties to be retrieved and
         // displayed.
-        buttonProtoNode.expand();
         buttonAsProtoProtoNode.expand();
-        documentProtoNode.expand();
 
-        is(buttonProtoNode.expanded, true,
-          "The buttonProtoNode should be expanded at this point.");
         is(buttonAsProtoProtoNode.expanded, true,
           "The buttonAsProtoProtoNode should be expanded at this point.");
-        is(documentProtoNode.expanded, true,
-          "The documentProtoNode should be expanded at this point.");
 
 
         // Poll every few milliseconds until the properties are retrieved.
         // It's important to set the timer in the chrome window, because the
         // content window timers are disabled while the debuggee is paused.
         let count2 = 0;
         let intervalID1 = window.setInterval(function(){
           info("count2: " + count2);
           if (++count2 > 50) {
             ok(false, "Timed out while polling for the properties.");
             window.clearInterval(intervalID1);
             return resumeAndFinish();
           }
-          if (!buttonProtoNode._retrieved ||
-              !buttonAsProtoProtoNode._retrieved ||
-              !documentProtoNode._retrieved) {
+          if (!buttonAsProtoProtoNode._retrieved) {
             return;
           }
           window.clearInterval(intervalID1);
 
-          // Now the main course: make sure that the native getters for WebIDL
-          // attributes have been called and a value has been returned.
-          is(buttonProtoNode.get("type").target.querySelector(".name")
+          // Test this more involved case that reuses an object that is
+          // present in another cache line.
+          is(buttonAsProtoProtoNode.get("type").target.querySelector(".name")
              .getAttribute("value"), "type",
-            "Should have the right property name for 'type' in buttonProtoNode.");
-          is(buttonProtoNode.get("type").target.querySelector(".value")
+            "Should have the right property name for 'type' in buttonAsProtoProtoProtoNode.");
+          is(buttonAsProtoProtoNode.get("type").target.querySelector(".value")
              .getAttribute("value"), '"submit"',
-            "'type' in buttonProtoNode should have the right value.");
-          is(buttonProtoNode.get("formMethod").target.querySelector(".name")
+            "'type' in buttonAsProtoProtoProtoNode should have the right value.");
+          is(buttonAsProtoProtoNode.get("formMethod").target.querySelector(".name")
              .getAttribute("value"), "formMethod",
-            "Should have the right property name for 'formMethod' in buttonProtoNode.");
-          is(buttonProtoNode.get("formMethod").target.querySelector(".value")
+            "Should have the right property name for 'formMethod' in buttonAsProtoProtoProtoNode.");
+          is(buttonAsProtoProtoNode.get("formMethod").target.querySelector(".value")
              .getAttribute("value"), '""',
-            "'formMethod' in buttonProtoNode should have the right value.");
-
-          is(documentProtoNode.get("domain").target.querySelector(".name")
-             .getAttribute("value"), "domain",
-            "Should have the right property name for 'domain' in documentProtoNode.");
-          is(documentProtoNode.get("domain").target.querySelector(".value")
-             .getAttribute("value"), '"example.com"',
-            "'domain' in documentProtoNode should have the right value.");
-          is(documentProtoNode.get("cookie").target.querySelector(".name")
-             .getAttribute("value"), "cookie",
-            "Should have the right property name for 'cookie' in documentProtoNode.");
-          is(documentProtoNode.get("cookie").target.querySelector(".value")
-             .getAttribute("value"), '""',
-            "'cookie' in documentProtoNode should have the right value.");
-
-          let buttonAsProtoProtoProtoNode = buttonAsProtoProtoNode.get("__proto__");
-
-          is(buttonAsProtoProtoProtoNode.expanded, false,
-            "The buttonAsProtoProtoProtoNode should not be expanded at this point.");
-
-          // Expand the prototype of the prototype of 'buttonAsProto' tree
-          // node. This causes its properties to be retrieved and displayed.
-          buttonAsProtoProtoProtoNode.expand();
+            "'formMethod' in buttonAsProtoProtoProtoNode should have the right value.");
 
-          is(buttonAsProtoProtoProtoNode.expanded, true,
-            "The buttonAsProtoProtoProtoNode should be expanded at this point.");
-
-          // Poll every few milliseconds until the properties are retrieved.
-          // It's important to set the timer in the chrome window, because the
-          // content window timers are disabled while the debuggee is paused.
-          let count3 = 0;
-          let intervalID2 = window.setInterval(function(){
-            info("count3: " + count3);
-            if (++count3 > 50) {
-              ok(false, "Timed out while polling for the properties.");
-              window.clearInterval(intervalID2);
-              return resumeAndFinish();
-            }
-            if (!buttonAsProtoProtoProtoNode._retrieved) {
-              return;
-            }
-            window.clearInterval(intervalID2);
-
-            // Test this more involved case that reuses an object that is
-            // present in another cache line.
-            is(buttonAsProtoProtoProtoNode.get("type").target.querySelector(".name")
-               .getAttribute("value"), "type",
-              "Should have the right property name for 'type' in buttonAsProtoProtoProtoNode.");
-            is(buttonAsProtoProtoProtoNode.get("type").target.querySelector(".value")
-               .getAttribute("value"), '"submit"',
-              "'type' in buttonAsProtoProtoProtoNode should have the right value.");
-            is(buttonAsProtoProtoProtoNode.get("formMethod").target.querySelector(".name")
-               .getAttribute("value"), "formMethod",
-              "Should have the right property name for 'formMethod' in buttonAsProtoProtoProtoNode.");
-            is(buttonAsProtoProtoProtoNode.get("formMethod").target.querySelector(".value")
-               .getAttribute("value"), '""',
-              "'formMethod' in buttonAsProtoProtoProtoNode should have the right value.");
-
-            resumeAndFinish();
-          }, 100);
+          resumeAndFinish();
         }, 100);
       }, 100);
     }}, 0);
   }, false);
 
   EventUtils.sendMouseEvent({ type: "click" },
     content.document.querySelector("button"),
     content.window);
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js
@@ -144,47 +144,45 @@ function testVariablesFiltering()
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
       "There should be 1 variable displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the load scope");
-    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
-      "There should be 1 variable displayed in the global scope");
+    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
+      "There should be 3 variables displayed in the global scope");
 
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 6,
-      "There should be 6 properties displayed in the inner scope");
+    ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 6,
+      "There should be more than 6 properties displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the load scope");
     is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the global scope");
 
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "this", "The only inner variable displayed should be 'this'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
-      "window", "The first inner property displayed should be 'window'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
-      "document", "The second inner property displayed should be 'document'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
-      "location", "The third inner property displayed should be 'location'");
+      "window", "The third inner property displayed should be 'window'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
-      "__proto__", "The fourth inner property displayed should be '__proto__'");
+      "document", "The fourth inner property displayed should be 'document'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[4].getAttribute("value"),
-      "Location", "The fifth inner property displayed should be 'Location'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[5].getAttribute("value"),
-      "Location", "The sixth inner property displayed should be 'Location'");
+      "location", "The fifth inner property displayed should be 'location'");
 
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
-      "Location", "The only global variable displayed should be 'Location'");
+      "location", "The first global variable displayed should be 'location'");
+    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
+      "locationbar", "The second global variable displayed should be 'locationbar'");
+    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
+      "Location", "The third global variable displayed should be 'Location'");
   }
 
   function test2()
   {
     innerScopeItem.collapse();
     mathScopeItem.collapse();
     testScopeItem.collapse();
     loadScopeItem.collapse();
@@ -232,47 +230,45 @@ function testVariablesFiltering()
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
       "There should be 1 variable displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the load scope");
-    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
-      "There should be 1 variable displayed in the global scope");
+    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
+      "There should be 3 variables displayed in the global scope");
 
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 6,
-      "There should be 6 properties displayed in the inner scope");
+    ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 6,
+      "There should be more than 6 properties displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the load scope");
     is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the global scope");
 
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "this", "The only inner variable displayed should be 'this'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
-      "window", "The first inner property displayed should be 'window'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
-      "document", "The second inner property displayed should be 'document'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
-      "location", "The third inner property displayed should be 'location'");
+      "window", "The third inner property displayed should be 'window'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
-      "__proto__", "The fourth inner property displayed should be '__proto__'");
+      "document", "The fourth inner property displayed should be 'document'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[4].getAttribute("value"),
-      "Location", "The fifth inner property displayed should be 'Location'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[5].getAttribute("value"),
-      "Location", "The sixth inner property displayed should be 'Location'");
+      "location", "The fifth inner property displayed should be 'location'");
 
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
-      "Location", "The only global variable displayed should be 'Location'");
+      "location", "The first global variable displayed should be 'location'");
+    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
+      "locationbar", "The second global variable displayed should be 'locationbar'");
+    is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[2].getAttribute("value"),
+      "Location", "The second global variable displayed should be 'Location'");
   }
 
   var scopes = gDebugger.DebuggerView.Variables._list,
       innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
       mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
       testScope = scopes.querySelectorAll(".variables-view-scope")[2],
       loadScope = scopes.querySelectorAll(".variables-view-scope")[3],
       globalScope = scopes.querySelectorAll(".variables-view-scope")[4];
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js
@@ -87,18 +87,18 @@ function testVariablesFiltering()
       "There should be 0 variables displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the load scope");
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 2,
       "There should be 2 variables displayed in the global scope");
 
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 8,
-      "There should be 8 properties displayed in the inner scope");
+    ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 3,
+      "There should be more than 3 properties displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the load scope");
     is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the global scope");
@@ -106,26 +106,16 @@ function testVariablesFiltering()
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "this", "The only inner variable displayed should be 'this'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "document", "The first inner property displayed should be 'document'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
       "window", "The second inner property displayed should be 'window'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
       "document", "The third inner property displayed should be 'document'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
-      "location", "The fourth inner property displayed should be 'location'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[4].getAttribute("value"),
-      "__proto__", "The fifth inner property displayed should be '__proto__'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[5].getAttribute("value"),
-      "__proto__", "The sixth inner property displayed should be '__proto__'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[6].getAttribute("value"),
-      "HTMLDocument", "The seventh inner property displayed should be 'HTMLDocument'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[7].getAttribute("value"),
-      "HTMLDocument", "The eight inner property displayed should be 'HTMLDocument'");
 
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "document", "The first global variable displayed should be 'document'");
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
       "HTMLDocument", "The first global variable displayed should be 'HTMLDocument'");
   }
 
   function test2()
@@ -163,36 +153,36 @@ function testVariablesFiltering()
     write("htmldocument");
 
     is(thisItem.expanded, true,
       "The local scope 'this' should be expanded");
     is(windowItem.expanded, true,
       "The local scope 'this.window' should be expanded");
     is(documentItem.expanded, true,
       "The local scope 'this.window.document' should be expanded");
-    is(locationItem.expanded, false,
-      "The local scope 'this.window.document.location' should not be expanded");
+    is(locationItem.expanded, true,
+      "The local scope 'this.window.document.location' should be expanded");
 
     documentItem.toggle();
     documentItem.toggle();
     locationItem.toggle();
 
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 1,
       "There should be 1 variable displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be 0 variables displayed in the load scope");
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 2,
       "There should be 2 variables displayed in the global scope");
 
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 8,
-      "There should be 8 properties displayed in the inner scope");
+    ok(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length > 3,
+      "There should be more than 3 properties displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the test scope");
     is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the load scope");
     is(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the global scope");
@@ -200,26 +190,16 @@ function testVariablesFiltering()
     is(innerScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "this", "The only inner variable displayed should be 'this'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "document", "The first inner property displayed should be 'document'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[1].getAttribute("value"),
       "window", "The second inner property displayed should be 'window'");
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[2].getAttribute("value"),
       "document", "The third inner property displayed should be 'document'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[3].getAttribute("value"),
-      "location", "The fourth inner property displayed should be 'location'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[4].getAttribute("value"),
-      "__proto__", "The fifth inner property displayed should be '__proto__'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[5].getAttribute("value"),
-      "__proto__", "The sixth inner property displayed should be '__proto__'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[6].getAttribute("value"),
-      "HTMLDocument", "The seventh inner property displayed should be 'HTMLDocument'");
-    is(innerScope.querySelectorAll(".variables-view-property:not([non-match]) > .title > .name")[7].getAttribute("value"),
-      "HTMLDocument", "The eight inner property displayed should be 'HTMLDocument'");
 
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "document", "The first global variable displayed should be 'document'");
     is(globalScope.querySelectorAll(".variables-view-variable:not([non-match]) > .title > .name")[1].getAttribute("value"),
       "HTMLDocument", "The first global variable displayed should be 'HTMLDocument'");
   }
 
   var scopes = gDebugger.DebuggerView.Variables._list,
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
@@ -122,18 +122,18 @@ function testVariablesFiltering()
       "There should be some variables displayed in the global scope");
 
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the test scope");
-    is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 1,
-      "There should be 1 property displayed in the load scope");
+    ok(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length > 1,
+      "There should be more than one property displayed in the load scope");
     isnot(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be some properties displayed in the global scope");
   }
 
   function test4()
   {
     backspace(1);
 
@@ -152,18 +152,18 @@ function testVariablesFiltering()
       "There should be some variables displayed in the global scope");
 
     is(innerScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the inner scope");
     is(mathScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the math scope");
     is(testScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be 0 properties displayed in the test scope");
-    is(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length, 1,
-      "There should be 1 property displayed in the load scope");
+    ok(loadScope.querySelectorAll(".variables-view-property:not([non-match])").length > 1,
+      "There should be more than one properties displayed in the load scope");
     isnot(globalScope.querySelectorAll(".variables-view-property:not([non-match])").length, 0,
       "There should be some properties displayed in the global scope");
   }
 
   var scopes = gDebugger.DebuggerView.Variables._list,
       innerScope = scopes.querySelectorAll(".variables-view-scope")[0],
       mathScope = scopes.querySelectorAll(".variables-view-scope")[1],
       testScope = scopes.querySelectorAll(".variables-view-scope")[2],
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -178,16 +178,17 @@ function debug_tab_pane(aURL, aOnDebuggi
     gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) {
       let dbg = toolbox.getCurrentPanel();
 
       // Wait for the initial resume...
       dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
         info("Debugger has started");
         dbg._view.Variables.lazyEmpty = false;
         dbg._view.Variables.lazyAppend = false;
+        dbg._view.Variables.lazyExpand = false;
         aOnDebugging(tab, debuggee, dbg);
       });
     });
   });
 }
 
 function debug_remote(aURL, aOnDebugging, aBeforeTabAdded) {
   // Make any necessary preparations (start the debugger server etc.)
@@ -201,16 +202,17 @@ function debug_remote(aURL, aOnDebugging
     info("Opening Remote Debugger");
     let win = DebuggerUI.toggleRemoteDebugger();
 
     // Wait for the initial resume...
     win.panelWin.gClient.addOneTimeListener("resumed", function() {
       info("Remote Debugger has started");
       win._dbgwin.DebuggerView.Variables.lazyEmpty = false;
       win._dbgwin.DebuggerView.Variables.lazyAppend = false;
+      win._dbgwin.DebuggerView.Variables.lazyExpand = false;
       aOnDebugging(tab, debuggee, win);
     });
   });
 }
 
 function debug_chrome(aURL, aOnClosing, aOnDebugging) {
   let tab = addTab(aURL, function() {
     let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
--- a/browser/devtools/debugger/test/helpers.js
+++ b/browser/devtools/debugger/test/helpers.js
@@ -16,21 +16,20 @@
 //   };
 // Now GCLI will emit output on every keypress that both explains the state
 // of GCLI and can be run as a test case.
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
-Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -217,18 +217,18 @@ DevTools.prototype = {
    * A definition is a light object that holds different information about a
    * developer tool. This object is not supposed to have any operational code.
    * See it as a "manifest".
    * The only actual code lives in the build() function, which will be used to
    * start an instance of this tool.
    *
    * Each toolDefinition has the following properties:
    * - id: Unique identifier for this tool (string|required)
-   * - killswitch: Property name to allow us to turn this tool on/off globally
-   *               (string|required) (TODO: default to devtools.{id}.enabled?)
+   * - visibilityswitch: Property name to allow us to hide this tool from the
+   *                     DevTools Toolbox.
    * - icon: URL pointing to a graphic which will be used as the src for an
    *         16x16 img tag (string|required)
    * - url: URL pointing to a XUL/XHTML document containing the user interface
    *        (string|required)
    * - label: Localized name for the tool to be displayed to the user
    *          (string|required)
    * - build: Function that takes an iframe, which has been populated with the
    *          markup from |url|, and also the toolbox containing the panel.
@@ -236,17 +236,17 @@ DevTools.prototype = {
    */
   registerTool: function DT_registerTool(toolDefinition) {
     let toolId = toolDefinition.id;
 
     if (!toolId || FORBIDDEN_IDS.has(toolId)) {
       throw new Error("Invalid definition.id");
     }
 
-    toolDefinition.killswitch = toolDefinition.killswitch ||
+    toolDefinition.visibilityswitch = toolDefinition.visibilityswitch ||
         "devtools." + toolId + ".enabled";
     this._tools.set(toolId, toolDefinition);
 
     this.emit("tool-registered", toolId);
   },
 
   /**
    * Removes all tools that match the given |toolId|
@@ -302,31 +302,27 @@ DevTools.prototype = {
    * Allow ToolBoxes to get at the list of tools that they should populate
    * themselves with.
    *
    * @return {Map} tools
    *         A map of the the tool definitions registered in this instance
    */
   getToolDefinitionMap: function DT_getToolDefinitionMap() {
     let tools = new Map();
-    let disabledTools = [];
-    try {
-      disabledTools = JSON.parse(Services.prefs.getCharPref("devtools.toolbox.disabledTools"));
-    } catch(ex) {}
 
     for (let [key, value] of this._tools) {
       let enabled;
 
       try {
-        enabled = Services.prefs.getBoolPref(value.killswitch);
+        enabled = Services.prefs.getBoolPref(value.visibilityswitch);
       } catch(e) {
         enabled = true;
       }
 
-      if (enabled && disabledTools.indexOf(key) == -1) {
+      if (enabled || value.id == "options") {
         tools.set(key, value);
       }
     }
     return tools;
   },
 
   /**
    * Tools have an inherent ordering that can't be represented in a Map so
@@ -449,17 +445,17 @@ DevTools.prototype = {
   },
 
   /**
    * All browser windows have been closed, tidy up remaining objects.
    */
   destroy: function() {
     Services.obs.removeObserver(this.destroy, "quit-application");
 
-    for (let [key, tool] of this._tools) {
+    for (let [key, tool] of this.getToolDefinitionMap()) {
       this.unregisterTool(key, true);
     }
 
     // Cleaning down the toolboxes: i.e.
     //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
     // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
   },
 };
@@ -573,25 +569,33 @@ let gDevToolsBrowser = {
       devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
     }
     devtoolsKeyset.appendChild(keys);
     let mainKeyset = doc.getElementById("mainKeyset");
     mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
   },
 
   /**
-   * Add the menuitem for a tool to all open browser windows. Also toggles the
-   * kill switch preference of the tool.
+   * Add the menuitem for a tool to all open browser windows.
    *
    * @param {object} toolDefinition
    *        properties of the tool to add
    */
   _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
-    // Set the kill switch pref boolean to true
-    Services.prefs.setBoolPref(toolDefinition.killswitch, true);
+    // No menu item or global shortcut is required for options panel.
+    if (toolDefinition.id == "options") {
+      return;
+    }
+
+    // Skip if the tool is disabled.
+    try {
+      if (!Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
+        return;
+      }
+    } catch(e) {}
 
     // We need to insert the new tool in the right place, which means knowing
     // the tool that comes before the tool that we're trying to add
     let allDefs = gDevTools.getToolDefinitionArray();
     let prevDef;
     for (let def of allDefs) {
       if (def === toolDefinition) {
         break;
@@ -637,16 +641,27 @@ let gDevToolsBrowser = {
   _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) {
     let fragCommands = doc.createDocumentFragment();
     let fragKeys = doc.createDocumentFragment();
     let fragBroadcasters = doc.createDocumentFragment();
     let fragAppMenuItems = doc.createDocumentFragment();
     let fragMenuItems = doc.createDocumentFragment();
 
     for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
+      if (toolDefinition.id == "options") {
+        continue;
+      }
+
+      // Skip if the tool is disabled.
+      try {
+        if (!Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
+          continue;
+        }
+      } catch(e) {}
+
       let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
 
       if (!elements) {
         return;
       }
 
       fragCommands.appendChild(elements.cmd);
       if (elements.key) {
@@ -709,17 +724,17 @@ let gDevToolsBrowser = {
       }
 
       key.setAttribute("command", cmd.id);
       key.setAttribute("modifiers", toolDefinition.modifiers);
     }
 
     let bc = doc.createElement("broadcaster");
     bc.id = "devtoolsMenuBroadcaster_" + id;
-    bc.setAttribute("label", toolDefinition.label);
+    bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label);
     bc.setAttribute("command", cmd.id);
 
     if (key) {
       bc.setAttribute("key", "key_" + id);
     }
 
     let appmenuitem = doc.createElement("menuitem");
     appmenuitem.id = "appmenuitem_" + id;
@@ -762,26 +777,22 @@ let gDevToolsBrowser = {
         broadcaster.setAttribute("checked", "true");
       } else {
         broadcaster.removeAttribute("checked");
       }
     }
   },
 
   /**
-   * Remove the menuitem for a tool to all open browser windows. Also sets the
-   * kill switch boolean pref to false.
+   * Remove the menuitem for a tool to all open browser windows.
    *
-   * @param {object} toolId
+   * @param {string} toolId
    *        id of the tool to remove
-   * @param {string} killswitch
-   *        The kill switch preference string of the tool
    */
-  _removeToolFromWindows: function DT_removeToolFromWindows(toolId, killswitch) {
-    Services.prefs.setBoolPref(killswitch, false);
+  _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
       gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
     }
   },
 
   /**
    * Remove a tool's menuitem from a window
    *
@@ -849,25 +860,20 @@ let gDevToolsBrowser = {
 this.gDevToolsBrowser = gDevToolsBrowser;
 
 gDevTools.on("tool-registered", function(ev, toolId) {
   let toolDefinition = gDevTools._tools.get(toolId);
   gDevToolsBrowser._addToolToWindows(toolDefinition);
 });
 
 gDevTools.on("tool-unregistered", function(ev, toolId) {
-  let killswitch;
-  if (typeof toolId == "string") {
-    killswitch = "devtools." + toolId + ".enabled";
-  }
-  else {
-    killswitch = toolId.killswitch;
+  if (typeof toolId != "string") {
     toolId = toolId.id;
   }
-  gDevToolsBrowser._removeToolFromWindows(toolId, killswitch);
+  gDevToolsBrowser._removeToolFromWindows(toolId);
 });
 
 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
 
 Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
 
 // Now load the tools.
--- a/browser/devtools/framework/test/browser_devtools_api.js
+++ b/browser/devtools/framework/test/browser_devtools_api.js
@@ -15,17 +15,17 @@ function test() {
     runTests(aTab);
   });
 }
 
 function runTests(aTab) {
   let toolDefinition = {
     id: toolId,
     isTargetSupported: function() true,
-    killswitch: "devtools.test-tool.enabled",
+    visibilityswitch: "devtools.test-tool.enabled",
     url: "about:blank",
     label: "someLabel",
     build: function(iframeWindow, toolbox) {
       let panel = new DevToolPanel(iframeWindow, toolbox);
       return panel.open();
     },
   };
 
--- a/browser/devtools/framework/test/browser_toolbox_options.js
+++ b/browser/devtools/framework/test/browser_toolbox_options.js
@@ -21,34 +21,26 @@ function testSelectTool(aToolbox) {
   toolbox = aToolbox;
   doc = toolbox.doc;
   toolbox.once("options-selected", testOptionsShortcut);
   toolbox.selectTool("options");
 }
 
 function testOptionsShortcut() {
   ok(true, "Toolbox selected via selectTool method");
-  toolbox.once("options-selected", testOptionsButtonClick);
+  toolbox.once("options-selected", testOptions);
   toolbox.selectTool("webconsole")
          .then(() => synthesizeKeyFromKeyTag("toolbox-options-key", doc));
 }
 
-function testOptionsButtonClick() {
-  ok(true, "Toolbox selected via shortcut");
-  toolbox.once("options-selected", testOptions);
-  toolbox.selectTool("webconsole")
-         .then(() => doc.getElementById("toolbox-tab-options").click());
-}
-
-function testOptions(event, iframe) {
+function testOptions(event, tool) {
   ok(true, "Toolbox selected via button click");
-  panelWin = iframe.contentWindow;
-  let panelDoc = iframe.contentDocument;
+  panelWin = tool.panelWin;
   // Testing pref changes
-  let prefCheckboxes = panelDoc.querySelectorAll("checkbox[data-pref]");
+  let prefCheckboxes = tool.panelDoc.querySelectorAll("checkbox[data-pref]");
   for (let checkbox of prefCheckboxes) {
     prefNodes.push(checkbox);
     prefValues.push(Services.prefs.getBoolPref(checkbox.getAttribute("data-pref")));
   }
   // Do again with opposite values to reset prefs
   for (let checkbox of prefCheckboxes) {
     prefNodes.push(checkbox);
     prefValues.push(!Services.prefs.getBoolPref(checkbox.getAttribute("data-pref")));
--- a/browser/devtools/framework/test/browser_toolbox_sidebar.js
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js
@@ -19,17 +19,17 @@ function test() {
 
   let panelDoc;
   let tab1Selected = false;
   let registeredTabs = {};
   let readyTabs = {};
 
   let toolDefinition = {
     id: "fakeTool4242",
-    killswitch: "devtools.fakeTool4242.enabled",
+    visibilityswitch: "devtools.fakeTool4242.enabled",
     url: toolURL,
     label: "FAKE TOOL!!!",
     isTargetSupported: function() true,
     build: function(iframeWindow, toolbox) {
       let deferred = Promise.defer();
       executeSoon(function() {
         deferred.resolve({
           target: toolbox.target,
--- a/browser/devtools/framework/test/browser_toolbox_window_title_changes.js
+++ b/browser/devtools/framework/test/browser_toolbox_window_title_changes.js
@@ -11,17 +11,17 @@ function test() {
   waitForExplicitFinish();
 
   const URL_1 = "data:text/plain;charset=UTF-8,abcde";
   const URL_2 = "data:text/plain;charset=UTF-8,12345";
 
   const TOOL_ID_1 = "webconsole";
   const TOOL_ID_2 = "jsdebugger";
 
-  const LABEL_1 = "Web Console";
+  const LABEL_1 = "Console";
   const LABEL_2 = "Debugger";
 
   let toolbox;
 
   addTab(URL_1, function () {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
       .then(function (aToolbox) { toolbox = aToolbox; })
--- a/browser/devtools/framework/toolbox-options.js
+++ b/browser/devtools/framework/toolbox-options.js
@@ -1,110 +1,142 @@
-/* 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 { utils: Cu } = Components;
-const DISABLED_TOOLS = "devtools.toolbox.disabledTools";
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/gDevTools.jsm");
-
-window.addEventListener("load", function onLoad() {
-  window.removeEventListener("load", onLoad);
-  setupToolsList();
-  populatePreferences();
-});
-
-function setupToolsList() {
-  let disabledTools = [];
-  try {
-    disabledTools = JSON.parse(Services.prefs.getCharPref(DISABLED_TOOLS));
-  } catch(ex) {
-    Cu.reportError("Error parsing pref " + DISABLED_TOOLS + " as JSON.");
-  }
-  let defaultToolsBox = document.getElementById("default-tools-box");
-  let additionalToolsBox = document.getElementById("additional-tools-box");
-
-  defaultToolsBox.textContent = "";
-  additionalToolsBox.textContent = "";
-
-  let onCheckboxClick = function(id) {
-    if (disabledTools.indexOf(id) > -1) {
-      disabledTools.splice(disabledTools.indexOf(id), 1);
-      Services.prefs.setCharPref(DISABLED_TOOLS, JSON.stringify(disabledTools));
-      gDevTools.emit("tool-registered", id);
-    }
-    else {
-      disabledTools.push(id);
-      Services.prefs.setCharPref(DISABLED_TOOLS, JSON.stringify(disabledTools));
-      gDevTools.emit("tool-unregistered", gDevTools._tools.get(id));
-    }
-  };
-
-  // Populating the default tools lists
-  for (let tool of gDevTools.getDefaultTools()) {
-    let checkbox = document.createElement("checkbox");
-    checkbox.setAttribute("id", tool.id);
-    checkbox.setAttribute("label", tool.label);
-    checkbox.setAttribute("tooltiptext", tool.tooltip || "");
-    checkbox.setAttribute("checked", disabledTools.indexOf(tool.id) == -1);
-    checkbox.addEventListener("command", onCheckboxClick.bind(null, tool.id));
-    defaultToolsBox.appendChild(checkbox);
-  }
-
-  // Populating the additional tools list that came from add-ons.
-  let atleastOneAddon = false;
-  for (let tool of gDevTools.getAdditionalTools()) {
-    atleastOneAddon = true;
-    let checkbox = document.createElement("checkbox");
-    checkbox.setAttribute("id", tool.id);
-    checkbox.setAttribute("label", tool.label);
-    checkbox.setAttribute("tooltiptext", tool.tooltip || "");
-    checkbox.setAttribute("checked", disabledTools.indexOf(tool.id) == -1);
-    checkbox.addEventListener("command", onCheckboxClick.bind(null, tool.id));
-    additionalToolsBox.appendChild(checkbox);
-  }
-
-  if (!atleastOneAddon) {
-    additionalToolsBox.style.display = "none";
-    additionalToolsBox.previousSibling.style.display = "none";
-  }
-
-  window.focus();
-}
-
-function populatePreferences() {
-  let prefCheckboxes = document.querySelectorAll("checkbox[data-pref]");
-  for (let checkbox of prefCheckboxes) {
-    checkbox.checked = Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"));
-    checkbox.addEventListener("command", function() {
-      let data = {
-        pref: this.getAttribute("data-pref"),
-        newValue: this.checked
-      };
-      data.oldValue = Services.prefs.getBoolPref(data.pref);
-      Services.prefs.setBoolPref(data.pref, data.newValue);
-      gDevTools.emit("pref-changed", data);
-    }.bind(checkbox));
-  }
-  let prefRadiogroups = document.querySelectorAll("radiogroup[data-pref]");
-  for (let radiogroup of prefRadiogroups) {
-    let selectedValue = Services.prefs.getCharPref(radiogroup.getAttribute("data-pref"));
-    for (let radio of radiogroup.childNodes) {
-      radiogroup.selectedIndex = -1;
-      if (radio.getAttribute("value") == selectedValue) {
-        radiogroup.selectedItem = radio;
-        break;
-      }
-    }
-    radiogroup.addEventListener("select", function() {
-      let data = {
-        pref: this.getAttribute("data-pref"),
-        newValue: this.selectedItem.getAttribute("value")
-      };
-      data.oldValue = Services.prefs.getCharPref(data.pref);
-      Services.prefs.setCharPref(data.pref, data.newValue);
-      gDevTools.emit("pref-changed", data);
-    }.bind(radiogroup));
-  }
-}
+/* 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 {Cu} = require("chrome");
+
+let Promise = require("sdk/core/promise");
+let EventEmitter = require("devtools/shared/event-emitter");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+exports.OptionsPanel = OptionsPanel;
+
+/**
+ * Represents the Options Panel in the Toolbox.
+ */
+function OptionsPanel(iframeWindow, toolbox) {
+  this.panelDoc = iframeWindow.document;
+  this.panelWin = iframeWindow;
+
+  EventEmitter.decorate(this);
+};
+
+OptionsPanel.prototype = {
+
+  open: function OP_open() {
+    let deferred = Promise.defer();
+
+    this.setupToolsList();
+    this.populatePreferences();
+
+    this.emit("ready");
+    deferred.resolve(this);
+    return deferred.promise;
+  },
+
+  setupToolsList: function OP_setupToolsList() {
+    let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
+    let additionalToolsBox = this.panelDoc.getElementById("additional-tools-box");
+
+    defaultToolsBox.textContent = "";
+    additionalToolsBox.textContent = "";
+
+    let pref = function(key) {
+      try {
+        return Services.prefs.getBoolPref(key);
+      }
+      catch (ex) {
+        return true;
+      }
+    };
+
+    let onCheckboxClick = function(id) {
+      let toolDefinition = gDevTools._tools.get(id);
+      // Set the kill switch pref boolean to true
+      Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
+      if (this.checked) {
+        gDevTools.emit("tool-registered", id);
+      }
+      else {
+        gDevTools.emit("tool-unregistered", toolDefinition);
+      }
+    };
+
+    // Populating the default tools lists
+    for (let tool of gDevTools.getDefaultTools()) {
+      if (tool.id == "options") {
+        continue;
+      }
+      let checkbox = this.panelDoc.createElement("checkbox");
+      checkbox.setAttribute("id", tool.id);
+      checkbox.setAttribute("label", tool.label);
+      checkbox.setAttribute("tooltiptext", tool.tooltip || "");
+      checkbox.setAttribute("checked", pref(tool.visibilityswitch));
+      checkbox.addEventListener("command", onCheckboxClick.bind(checkbox, tool.id));
+      defaultToolsBox.appendChild(checkbox);
+    }
+
+    // Populating the additional tools list that came from add-ons.
+    let atleastOneAddon = false;
+    for (let tool of gDevTools.getAdditionalTools()) {
+      atleastOneAddon = true;
+      let checkbox = this.panelDoc.createElement("checkbox");
+      checkbox.setAttribute("id", tool.id);
+      checkbox.setAttribute("label", tool.label);
+      checkbox.setAttribute("tooltiptext", tool.tooltip || "");
+      checkbox.setAttribute("checked", pref(tool.visibilityswitch));
+      checkbox.addEventListener("command", onCheckboxClick.bind(checkbox, tool.id));
+      additionalToolsBox.appendChild(checkbox);
+    }
+
+    if (!atleastOneAddon) {
+      additionalToolsBox.style.display = "none";
+      additionalToolsBox.previousSibling.style.display = "none";
+    }
+
+    this.panelWin.focus();
+  },
+
+  populatePreferences: function OP_populatePreferences() {
+    let prefCheckboxes = this.panelDoc.querySelectorAll("checkbox[data-pref]");
+    for (let checkbox of prefCheckboxes) {
+      checkbox.checked = Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"));
+      checkbox.addEventListener("command", function() {
+        let data = {
+          pref: this.getAttribute("data-pref"),
+          newValue: this.checked
+        };
+        data.oldValue = Services.prefs.getBoolPref(data.pref);
+        Services.prefs.setBoolPref(data.pref, data.newValue);
+        gDevTools.emit("pref-changed", data);
+      }.bind(checkbox));
+    }
+    let prefRadiogroups = this.panelDoc.querySelectorAll("radiogroup[data-pref]");
+    for (let radiogroup of prefRadiogroups) {
+      let selectedValue = Services.prefs.getCharPref(radiogroup.getAttribute("data-pref"));
+      for (let radio of radiogroup.childNodes) {
+        radiogroup.selectedIndex = -1;
+        if (radio.getAttribute("value") == selectedValue) {
+          radiogroup.selectedItem = radio;
+          break;
+        }
+      }
+      radiogroup.addEventListener("select", function() {
+        let data = {
+          pref: this.getAttribute("data-pref"),
+          newValue: this.selectedItem.getAttribute("value")
+        };
+        data.oldValue = Services.prefs.getCharPref(data.pref);
+        Services.prefs.setCharPref(data.pref, data.newValue);
+        gDevTools.emit("pref-changed", data);
+      }.bind(radiogroup));
+    }
+  },
+
+  destroy: function OP_destroy() {
+    this.panelWin = this.panelDoc = null;
+  }
+};
--- a/browser/devtools/framework/toolbox-options.xul
+++ b/browser/devtools/framework/toolbox-options.xul
@@ -6,17 +6,16 @@
 <!ENTITY % toolboxDTD SYSTEM "chrome://browser/locale/devtools/toolbox.dtd" >
  %toolboxDTD;
 ]>
 <?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
 <?xml-stylesheet rel="stylesheet" href="chrome://browser/content/devtools/framework/toolbox.css" type="text/css"?>
 <?xml-stylesheet rel="stylesheet" href="chrome://browser/skin/devtools/toolbox.css" type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-  <script type="application/javascript;version=1.8" src="toolbox-options.js"></script>
   <hbox id="options-panel-container" flex="1">
     <hbox id="options-panel" flex="1">
       <vbox id="tools-box" class="options-vertical-pane" flex="1">
         <label value="&options.selectDefaultTools.label;"/>
         <vbox id="default-tools-box" class="options-groupbox" tabindex="0"/>
         <label value="&options.selectAdditionalTools.label;"/>
         <vbox id="additional-tools-box" class="options-groupbox"/>
       </vbox>
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -32,17 +32,17 @@ XPCOMUtils.defineLazyGetter(this, "toolb
     }
   };
   return l10n;
 });
 
 XPCOMUtils.defineLazyGetter(this, "Requisition", function() {
   let scope = {};
   Cu.import("resource://gre/modules/devtools/Require.jsm", scope);
-  Cu.import("resource:///modules/devtools/gcli.jsm", scope);
+  Cu.import("resource://gre/modules/devtools/gcli.jsm", {});
 
   let req = scope.require;
   return req('gcli/cli').Requisition;
 });
 
 /**
  * A "Toolbox" is the component that holds all the tools for one specific
  * target. Visually, it's a document that includes the tools tabs and all
@@ -215,24 +215,16 @@ Toolbox.prototype = {
       iframe.addEventListener("DOMContentLoaded", domReady, true);
       iframe.setAttribute("src", this._URL);
     }.bind(this));
 
     return deferred.promise;
   },
 
   _buildOptions: function TBOX__buildOptions() {
-    this.optionsButton = this.doc.getElementById("toolbox-tab-options");
-    this.optionsButton.addEventListener("command", function() {
-      this.selectTool("options");
-    }.bind(this), false);
-
-    let iframe = this.doc.getElementById("toolbox-panel-iframe-options");
-    this._toolPanels.set("options", iframe);
-
     let key = this.doc.getElementById("toolbox-options-key");
     key.addEventListener("command", function(toolId) {
       this.selectTool(toolId);
     }.bind(this, "options"), true);
   },
 
   /**
    * Adds the keys and commands to the Toolbox Window in window mode.
@@ -353,58 +345,64 @@ Toolbox.prototype = {
     let tabs = this.doc.getElementById("toolbox-tabs");
     let deck = this.doc.getElementById("toolbox-deck");
 
     let id = toolDefinition.id;
 
     let radio = this.doc.createElement("radio");
     radio.className = "toolbox-tab devtools-tab";
     radio.id = "toolbox-tab-" + id;
-    radio.setAttribute("flex", "1");
     radio.setAttribute("toolid", id);
     if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
       toolDefinition.ordinal = MAX_ORDINAL;
     }
     radio.setAttribute("ordinal", toolDefinition.ordinal);
     radio.setAttribute("tooltiptext", toolDefinition.tooltip);
 
     radio.addEventListener("command", function(id) {
       this.selectTool(id);
     }.bind(this, id));
 
+    // spacer lets us center the image and label, while allowing cropping
+    let spacer = this.doc.createElement("spacer");
+    spacer.setAttribute("flex", "1");
+    radio.appendChild(spacer);
+
     if (toolDefinition.icon) {
       let image = this.doc.createElement("image");
       image.setAttribute("src", toolDefinition.icon);
       radio.appendChild(image);
     }
 
-    let label = this.doc.createElement("label");
-    label.setAttribute("value", toolDefinition.label)
-    label.setAttribute("crop", "end");
-    label.setAttribute("flex", "1");
+    if (toolDefinition.label) {
+      let label = this.doc.createElement("label");
+      label.setAttribute("value", toolDefinition.label)
+      label.setAttribute("crop", "end");
+      label.setAttribute("flex", "1");
+      radio.appendChild(label);
+      radio.setAttribute("flex", "1");
+    }
 
     let vbox = this.doc.createElement("vbox");
     vbox.className = "toolbox-panel";
     vbox.id = "toolbox-panel-" + id;
 
-    radio.appendChild(label);
 
     // If there is no tab yet, or the ordinal to be added is the largest one.
     if (tabs.childNodes.length == 0 ||
         +tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
       tabs.appendChild(radio);
       deck.appendChild(vbox);
     }
     // else, iterate over all the tabs to get the correct location.
     else {
       Array.some(tabs.childNodes, (node, i) => {
         if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
           tabs.insertBefore(radio, node);
-          deck.insertBefore(vbox, deck.childNodes[i + 1]);
-          // + 1 because of options panel.
+          deck.insertBefore(vbox, deck.childNodes[i]);
           return true;
         }
       });
     }
 
     this._addKeysToWindow();
   },
 
@@ -435,38 +433,31 @@ Toolbox.prototype = {
     let tab = this.doc.getElementById("toolbox-tab-" + id);
 
     if (!tab) {
       throw new Error("No tool found");
     }
 
     let tabstrip = this.doc.getElementById("toolbox-tabs");
 
-    // select the right tab
-    let index = -1;
+    // select the right tab, making 0th index the default tab if right tab not
+    // found
+    let index = 0;
     let tabs = tabstrip.childNodes;
     for (let i = 0; i < tabs.length; i++) {
       if (tabs[i] === tab) {
         index = i;
         break;
       }
     }
     tabstrip.selectedItem = tab;
 
     // and select the right iframe
     let deck = this.doc.getElementById("toolbox-deck");
-    // offset by 1 due to options panel
-    if (id == "options") {
-      deck.selectedIndex = 0;
-      this.optionsButton.setAttribute("checked", true);
-    }
-    else {
-      deck.selectedIndex = index != -1 ? index + 1: -1;
-      this.optionsButton.removeAttribute("checked");
-    }
+    deck.selectedIndex = index;
 
     let definition = gDevTools.getToolDefinitionMap().get(id);
 
     this._currentToolId = id;
 
     let resolveSelected = panel => {
       this.emit("select", id);
       this.emit(id + "-selected", panel);
@@ -663,17 +654,17 @@ Toolbox.prototype = {
     }
 
     if (panel) {
       panel.parentNode.removeChild(panel);
     }
 
     if (this.hostType == Toolbox.HostType.WINDOW) {
       let doc = this.doc.defaultView.parent.document;
-      let key = doc.getElementById("key_" + id);
+      let key = doc.getElementById("key_" + toolId);
       if (key) {
         key.parentNode.removeChild(key);
       }
     }
   },
 
 
   /**
@@ -704,17 +695,16 @@ Toolbox.prototype = {
     this.off("select", this._refreshHostTitle);
     this.off("host-changed", this._refreshHostTitle);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
     let outstanding = [];
 
-    this._toolPanels.delete("options");
     for (let [id, panel] of this._toolPanels) {
       outstanding.push(panel.destroy());
     }
 
     let container = this.doc.getElementById("toolbox-buttons");
     while(container.firstChild) {
       container.removeChild(container.firstChild);
     }
--- a/browser/devtools/framework/toolbox.xul
+++ b/browser/devtools/framework/toolbox.xul
@@ -29,36 +29,24 @@
     <toolbar class="devtools-tabbar">
 #ifdef XP_MACOSX
       <hbox id="toolbox-controls">
         <toolbarbutton id="toolbox-close"
                        tooltiptext="&toolboxCloseButton.tooltip;"/>
         <hbox id="toolbox-dock-buttons"/>
       </hbox>
 #endif
-      <toolbarbutton id="toolbox-tab-options"
-                     autocheck="false"
-                     class="command-button toolbox-tab devtools-tab"
-                     tooltiptext="&toolboxOptionsButton.tooltip;"/>
       <hbox id="toolbox-tabs" flex="1">
       </hbox>
       <hbox id="toolbox-buttons" pack="end"/>
 #ifndef XP_MACOSX
       <vbox id="toolbox-controls-separator"/>
       <hbox id="toolbox-controls">
         <hbox id="toolbox-dock-buttons"/>
         <toolbarbutton id="toolbox-close"
                        tooltiptext="&toolboxCloseButton.tooltip;"/>
       </hbox>
 #endif
     </toolbar>
     <deck id="toolbox-deck" flex="1">
-      <vbox id="toolbox-panel-options"
-            class="toolbox-panel">
-        <iframe id="toolbox-panel-iframe-options"
-                class="toolbox-panel-iframe"
-                flex="1" forceOwnRefreshDriver=""
-                src="chrome://browser/content/devtools/framework/toolbox-options.xul">
-        </iframe>
-      </vbox>
     </deck>
   </notificationbox>
 </window>
--- a/browser/devtools/inspector/CmdInspect.jsm
+++ b/browser/devtools/inspector/CmdInspect.jsm
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 this.EXPORTED_SYMBOLS = [ ];
 
-Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 
 /**
--- a/browser/devtools/inspector/test/helpers.js
+++ b/browser/devtools/inspector/test/helpers.js
@@ -16,21 +16,20 @@
 //   };
 // Now GCLI will emit output on every keypress that both explains the state
 // of GCLI and can be run as a test case.
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
-Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -24,108 +24,127 @@ Object.defineProperty(exports, "Toolbox"
 });
 Object.defineProperty(exports, "TargetFactory", {
   get: () => require("devtools/framework/target").TargetFactory
 });
 
 loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
 
 // Panels
+loader.lazyGetter(this, "OptionsPanel", function() require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", function() require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm");
 loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.jsm");
 loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
 loader.lazyImporter(this, "ProfilerPanel", "resource:///modules/devtools/ProfilerPanel.jsm");
 loader.lazyImporter(this, "NetMonitorPanel", "resource:///modules/devtools/NetMonitorPanel.jsm");
 
 // Strings
+const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
 const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
 const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
 const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
 const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
 const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
+loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
 loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
 loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
 loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
 loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
 loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
 loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
 
 let Tools = {};
 exports.Tools = Tools;
 
 // Definitions
+Tools.options = {
+  id: "options",
+  ordinal: 0,
+  url: "chrome://browser/content/devtools/framework/toolbox-options.xul",
+  icon: "chrome://browser/skin/devtools/tool-options.png",
+  tooltip: l10n("optionsButton.tooltip", toolboxStrings),
+  isTargetSupported: function(target) {
+    return true;
+  },
+  build: function(iframeWindow, toolbox) {
+    let panel = new OptionsPanel(iframeWindow, toolbox);
+    return panel.open();
+  }
+}
+
 Tools.webConsole = {
   id: "webconsole",
   key: l10n("cmd.commandkey", webConsoleStrings),
   accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
   modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
-  ordinal: 0,
+  ordinal: 1,
   icon: "chrome://browser/skin/devtools/tool-webconsole.png",
   url: "chrome://browser/content/devtools/webconsole.xul",
-  label: l10n("ToolboxWebconsole.label", webConsoleStrings),
+  label: l10n("ToolboxTabWebconsole.label", webConsoleStrings),
+  menuLabel: l10n("MenuWebconsole.label", webConsoleStrings),
   tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings),
 
   isTargetSupported: function(target) {
     return true;
   },
   build: function(iframeWindow, toolbox) {
     let panel = new WebConsolePanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
-Tools.jsdebugger = {
-  id: "jsdebugger",
-  key: l10n("open.commandkey", debuggerStrings),
-  accesskey: l10n("debuggerMenu.accesskey", debuggerStrings),
-  modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
-  ordinal: 2,
-  killswitch: "devtools.debugger.enabled",
-  icon: "chrome://browser/skin/devtools/tool-debugger.png",
-  url: "chrome://browser/content/devtools/debugger.xul",
-  label: l10n("ToolboxDebugger.label", debuggerStrings),
-  tooltip: l10n("ToolboxDebugger.tooltip", debuggerStrings),
-
-  isTargetSupported: function(target) {
-    return true;
-  },
-
-  build: function(iframeWindow, toolbox) {
-    let panel = new DebuggerPanel(iframeWindow, toolbox);
-    return panel.open();
-  }
-};
-
 Tools.inspector = {
   id: "inspector",
   accesskey: l10n("inspector.accesskey", inspectorStrings),
   key: l10n("inspector.commandkey", inspectorStrings),
-  ordinal: 1,
+  ordinal: 2,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   icon: "chrome://browser/skin/devtools/tool-inspector.png",
   url: "chrome://browser/content/devtools/inspector/inspector.xul",
   label: l10n("inspector.label", inspectorStrings),
   tooltip: l10n("inspector.tooltip", inspectorStrings),
 
   isTargetSupported: function(target) {
     return !target.isRemote;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new InspectorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
+Tools.jsdebugger = {
+  id: "jsdebugger",
+  key: l10n("open.commandkey", debuggerStrings),
+  accesskey: l10n("debuggerMenu.accesskey", debuggerStrings),
+  modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
+  ordinal: 3,
+  visibilityswitch: "devtools.debugger.enabled",
+  icon: "chrome://browser/skin/devtools/tool-debugger.png",
+  url: "chrome://browser/content/devtools/debugger.xul",
+  label: l10n("ToolboxDebugger.label", debuggerStrings),
+  tooltip: l10n("ToolboxDebugger.tooltip", debuggerStrings),
+
+  isTargetSupported: function(target) {
+    return true;
+  },
+
+  build: function(iframeWindow, toolbox) {
+    let panel = new DebuggerPanel(iframeWindow, toolbox);
+    return panel.open();
+  }
+};
+
 Tools.styleEditor = {
   id: "styleeditor",
   key: l10n("open.commandkey", styleEditorStrings),
-  ordinal: 3,
+  ordinal: 4,
   accesskey: l10n("open.accesskey", styleEditorStrings),
   modifiers: "shift",
   icon: "chrome://browser/skin/devtools/tool-styleeditor.png",
   url: "chrome://browser/content/devtools/styleeditor.xul",
   label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
   tooltip: l10n("ToolboxStyleEditor.tooltip", styleEditorStrings),
 
   isTargetSupported: function(target) {
@@ -137,19 +156,19 @@ Tools.styleEditor = {
     return panel.open();
   }
 };
 
 Tools.jsprofiler = {
   id: "jsprofiler",
   accesskey: l10n("profiler.accesskey", profilerStrings),
   key: l10n("profiler2.commandkey", profilerStrings),
-  ordinal: 4,
+  ordinal: 5,
   modifiers: "shift",
-  killswitch: "devtools.profiler.enabled",
+  visibilityswitch: "devtools.profiler.enabled",
   icon: "chrome://browser/skin/devtools/tool-profiler.png",
   url: "chrome://browser/content/devtools/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
   tooltip: l10n("profiler.tooltip", profilerStrings),
 
   isTargetSupported: function (target) {
     return true;
   },
@@ -159,35 +178,36 @@ Tools.jsprofiler = {
     return panel.open();
   }
 };
 
 Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
   key: l10n("netmonitor.commandkey", netMonitorStrings),
-  ordinal: 5,
+  ordinal: 6,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
-  killswitch: "devtools.netmonitor.enabled",
-  icon: "chrome://browser/skin/devtools/tool-profiler.png",
+  visibilityswitch: "devtools.netmonitor.enabled",
+  icon: "chrome://browser/skin/devtools/tool-network.png",
   url: "chrome://browser/content/devtools/netmonitor.xul",
   label: l10n("netmonitor.label", netMonitorStrings),
   tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
 
   isTargetSupported: function(target) {
     return true;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new NetMonitorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
 let defaultTools = [
+  Tools.options,
   Tools.styleEditor,
   Tools.webConsole,
   Tools.jsdebugger,
   Tools.inspector,
   Tools.jsprofiler,
   Tools.netMonitor
 ];
 
@@ -196,17 +216,17 @@ exports.defaultTools = defaultTools;
 for (let definition of defaultTools) {
   gDevTools.registerTool(definition);
 }
 
 var unloadObserver = {
   observe: function(subject, topic, data) {
     if (subject.wrappedJSObject === require("@loader/unload")) {
       Services.obs.removeObserver(unloadObserver, "sdk:loader:destroy");
-      for (let definition of defaultTools) {
+      for (let definition of gDevTools.getToolDefinitionArray()) {
         gDevTools.unregisterTool(definition.id);
       }
     }
   }
 };
 Services.obs.addObserver(unloadObserver, "sdk:loader:destroy", false);
 
 /**
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -12,17 +12,17 @@ const PAGE_SIZE = 10;
 const PREVIEW_AREA = 700;
 const DEFAULT_MAX_CHILDREN = 100;
 
 let {UndoStack} = require("devtools/shared/undo");
 let EventEmitter = require("devtools/shared/event-emitter");
 let {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
-Cu.import("resource:///modules/devtools/Templater.jsm");
+Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 /**
  * Vocabulary for the purposes of this file:
  *
  * MarkupContainer - the structure that holds an editor and its
  *  immediate children in the markup panel.
  * Node - A content node.
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -1,20 +1,28 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
 const EPSILON = 0.001;
+const RESIZE_REFRESH_RATE = 50; // ms
 const REQUESTS_REFRESH_RATE = 50; // ms
 const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px
-const REQUESTS_WATERFALL_SAFE_BOUNDS = 100; // px
-const REQUESTS_WATERFALL_BACKGROUND_PATTERN = [5, 250, 1000, 2000]; // ms
+const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 10; // byte
+const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 16; // byte
 const DEFAULT_HTTP_VERSION = "HTTP/1.1";
 const HEADERS_SIZE_DECIMALS = 3;
 const CONTENT_SIZE_DECIMALS = 2;
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
   "ecmascript": "js",
   "javascript": "js",
   "x-javascript": "js"
 };
@@ -43,18 +51,16 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = 
   descriptorTooltip: false,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChage: true,
   eval: () => {},
   switch: () => {}
 };
 
-function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
-
 /**
  * Object defining the network monitor view components.
  */
 let NetMonitorView = {
   /**
    * Initializes the network monitor view.
    *
    * @param function aCallback
@@ -152,16 +158,17 @@ let NetMonitorView = {
       button.setAttribute("pane-collapsed", "");
       button.setAttribute("tooltiptext", this._expandPaneString);
     }
 
     if (aTabIndex !== undefined) {
       $("#details-pane").selectedIndex = aTabIndex;
     }
   },
+
   /**
    * Lazily initializes and returns a promise for a SourceEditor instance.
    *
    * @param string aId
    *        The id of the editor placeholder node.
    * @return object
    *         A Promise that is resolved when the editor is available.
    */
@@ -259,16 +266,17 @@ create({ constructor: RequestsMenuView, 
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function NVRM_initialize() {
     dumpn("Initializing the RequestsMenuView");
 
     this.node = new SideMenuWidget($("#requests-menu-contents"), false);
     this.node.maintainSelectionVisible = false;
+    this.node.autoscrollWithAppendedItems = true;
 
     this.node.addEventListener("mousedown", this._onMouseDown, false);
     this.node.addEventListener("select", this._onSelect, false);
     window.addEventListener("resize", this._onResize, false);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
@@ -316,32 +324,154 @@ create({ constructor: RequestsMenuView, 
     let menuView = this._createMenuView(aMethod, aUrl);
 
     // Remember the first and last event boundaries.
     this._registerFirstRequestStart(unixTime);
     this._registerLastRequestEnd(unixTime);
 
     // Append a network request item to this container.
     let requestItem = this.push(menuView, {
-      relaxed: true, /* this container should allow dupes & degenerates */
       attachment: {
         id: aId,
         startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
         startedMillis: unixTime,
         method: aMethod,
         url: aUrl
       },
       finalize: this._onRequestItemRemoved
     });
 
+    $("#details-pane-toggle").disabled = false;
     $(".requests-menu-empty-notice").hidden = true;
+
     this._cache.set(aId, requestItem);
   },
 
   /**
+   * Sorts all network requests in this container by a specified detail.
+   *
+   * @param string aType
+   *        Either "status", "method", "file", "domain", "type" or "size".
+   */
+  sortBy: function NVRM_sortBy(aType) {
+    let target = $("#requests-menu-" + aType + "-button");
+    let headers = document.querySelectorAll(".requests-menu-header-button");
+
+    for (let header of headers) {
+      if (header != target) {
+        header.removeAttribute("sorted");
+        header.removeAttribute("tooltiptext");
+      }
+    }
+
+    let direction = "";
+    if (target) {
+      if (!target.hasAttribute("sorted")) {
+        target.setAttribute("sorted", direction = "ascending");
+        target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedAsc"));
+      } else if (target.getAttribute("sorted") == "ascending") {
+        target.setAttribute("sorted", direction = "descending");
+        target.setAttribute("tooltiptext", L10N.getStr("networkMenu.sortedDesc"));
+      } else {
+        target.removeAttribute("sorted");
+        target.removeAttribute("tooltiptext");
+      }
+    }
+
+    // Sort by timing.
+    if (!target || !direction) {
+      this.sortContents(this._byTiming);
+    }
+    // Sort by whatever was requested.
+    else switch (aType) {
+      case "status":
+        if (direction == "ascending") {
+          this.sortContents(this._byStatus);
+        } else {
+          this.sortContents((a, b) => !this._byStatus(a, b));
+        }
+        break;
+      case "method":
+        if (direction == "ascending") {
+          this.sortContents(this._byMethod);
+        } else {
+          this.sortContents((a, b) => !this._byMethod(a, b));
+        }
+        break;
+      case "file":
+        if (direction == "ascending") {
+          this.sortContents(this._byFile);
+        } else {
+          this.sortContents((a, b) => !this._byFile(a, b));
+        }
+        break;
+      case "domain":
+        if (direction == "ascending") {
+          this.sortContents(this._byDomain);
+        } else {
+          this.sortContents((a, b) => !this._byDomain(a, b));
+        }
+        break;
+      case "type":
+        if (direction == "ascending") {
+          this.sortContents(this._byType);
+        } else {
+          this.sortContents((a, b) => !this._byType(a, b));
+        }
+        break;
+      case "size":
+        if (direction == "ascending") {
+          this.sortContents(this._bySize);
+        } else {
+          this.sortContents((a, b) => !this._bySize(a, b));
+        }
+        break;
+    }
+  },
+
+  /**
+   * Predicates used when sorting items.
+   *
+   * @param MenuItem aFirst
+   *        The first menu item used in the comparison.
+   * @param MenuItem aSecond
+   *        The second menu item used in the comparison.
+   * @return number
+   *         -1 to sort aFirst to a lower index than aSecond
+   *          0 to leave aFirst and aSecond unchanged with respect to each other
+   *          1 to sort aSecond to a lower index than aFirst
+   */
+  _byTiming: (aFirst, aSecond) =>
+    aFirst.attachment.startedMillis > aSecond.attachment.startedMillis,
+
+  _byStatus: (aFirst, aSecond) =>
+    aFirst.attachment.status > aSecond.attachment.status,
+
+  _byMethod: (aFirst, aSecond) =>
+    aFirst.attachment.method > aSecond.attachment.method,
+
+  _byFile: (aFirst, aSecond) =>
+    !aFirst.target || !aSecond.target ? -1 :
+      $(".requests-menu-file", aFirst.target).getAttribute("value").toLowerCase() >
+      $(".requests-menu-file", aSecond.target).getAttribute("value").toLowerCase(),
+
+  _byDomain: (aFirst, aSecond) =>
+    !aFirst.target || !aSecond.target ? -1 :
+      $(".requests-menu-domain", aFirst.target).getAttribute("value").toLowerCase() >
+      $(".requests-menu-domain", aSecond.target).getAttribute("value").toLowerCase(),
+
+  _byType: (aFirst, aSecond) =>
+    !aFirst.target || !aSecond.target ? -1 :
+      $(".requests-menu-type", aFirst.target).getAttribute("value").toLowerCase() >
+      $(".requests-menu-type", aSecond.target).getAttribute("value").toLowerCase(),
+
+  _bySize: (aFirst, aSecond) =>
+    aFirst.attachment.contentSize > aSecond.attachment.contentSize,
+
+  /**
    * Schedules adding additional information to a network request.
    *
    * @param string aId
    *        An identifier coming from the network monitor controller.
    * @param object aData
    *        An object containing several { key: value } tuples of network info.
    *        Supported keys are "httpVersion", "status", "statusText" etc.
    */
@@ -351,18 +481,18 @@ create({ constructor: RequestsMenuView, 
       return;
     }
     this._updateQueue.push([aId, aData]);
 
     // Lazy updating is disabled in some tests.
     if (!this.lazyUpdate) {
       return void this._flushRequests();
     }
-    window.clearTimeout(this._updateTimeout);
-    this._updateTimeout = window.setTimeout(this._flushRequests, REQUESTS_REFRESH_RATE);
+    // Allow requests to settle down first.
+    drain("update-requests", REQUESTS_REFRESH_RATE, () => this._flushRequests());
   },
 
   /**
    * Starts adding all queued additional information about network requests.
    */
   _flushRequests: function NVRM__flushRequests() {
     // For each queued additional information packet, get the corresponding
     // request item in the view and update it based on the specified data.
@@ -439,18 +569,22 @@ create({ constructor: RequestsMenuView, 
       }
       // This update may have additional information about a request which
       // isn't shown yet in the network details pane.
       let selectedItem = this.selectedItem;
       if (selectedItem && selectedItem.attachment.id == id) {
         NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
       }
     }
+
     // We're done flushing all the requests, clear the update queue.
     this._updateQueue = [];
+
+    // Make sure all the requests are sorted.
+    this.sortContents();
   },
 
   /**
    * Customization function for creating an item's UI.
    *
    * @param string aMethod
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string aUrl
@@ -579,56 +713,42 @@ create({ constructor: RequestsMenuView, 
     startCapNode.hidden = false;
     endCapNode.hidden = false;
 
     // Rescale all the waterfalls so that everything is visible at once.
     this._flushWaterfallViews();
   },
 
   /**
-   * Rescales and redraws all the waterfalls in this container.
+   * Rescales and redraws all the waterfall views in this container.
    *
    * @param boolean aReset
    *        True if this container's width was changed.
    */
   _flushWaterfallViews: function NVRM__flushWaterfallViews(aReset) {
-    // To avoid expensive operations like getBoundingClientRect(), the
-    // waterfalls width is cached. However, in certain scenarios like when
-    // the window is resized, this needs to be invalidated.
+    // To avoid expensive operations like getBoundingClientRect() and
+    // rebuilding the waterfall background each time a new request comes in,
+    // stuff is cached. However, in certain scenarios like when the window
+    // is resized, this needs to be invalidated.
     if (aReset) {
       this._cachedWaterfallWidth = 0;
-
-      let table = $("#network-table");
-      let toolbar = $("#requests-menu-toolbar");
-      let columns = [
-        [".requests-menu-waterfall", "waterfall-overflows"],
-        [".requests-menu-size", "size-overflows"],
-        [".requests-menu-type", "type-overflows"],
-        [".requests-menu-domain", "domain-overflows"]
-      ];
-
-      // Flush headers.
-      columns.forEach(([, attribute]) => table.removeAttribute(attribute));
-      let availableWidth = toolbar.getBoundingClientRect().width;
-
-      // Hide overflowing columns.
-      columns.forEach(([className, attribute]) => {
-        let bounds = $(".requests-menu-header" + className).getBoundingClientRect();
-        if (bounds.right > availableWidth - REQUESTS_HEADERS_SAFE_BOUNDS) {
-          table.setAttribute(attribute, "");
-        }
-      });
+      this._hideOverflowingColumns();
     }
 
     // Determine the scaling to be applied to all the waterfalls so that
     // everything is visible at once. One millisecond == one unscaled pixel.
     let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
     let longestWidth = this._lastRequestEndedMillis - this._firstRequestStartedMillis;
     let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
 
+    // Redraw and set the canvas background for each waterfall view.
+    this._showWaterfallDivisionLabels(scale);
+    this._drawWaterfallBackground(scale);
+    this._flushWaterfallBackgrounds();
+
     // Apply CSS transforms to each waterfall in this container totalTime
     // accurately translate and resize as needed.
     for (let [, { target, attachment }] of this._cache) {
       let timingsNode = $(".requests-menu-timings", target);
       let startCapNode = $(".requests-menu-timings-cap.start", target);
       let endCapNode = $(".requests-menu-timings-cap.end", target);
       let totalNode = $(".requests-menu-timings-total", target);
 
@@ -647,16 +767,155 @@ create({ constructor: RequestsMenuView, 
       timingsNode.style.transform = scaleX + " " + translateX;
       startCapNode.style.transform = revScaleX + " translateX(0.5px)";
       endCapNode.style.transform = revScaleX + " translateX(-0.5px)";
       totalNode.style.transform = revScaleX;
     }
   },
 
   /**
+   * Creates the labels displayed on the waterfall header in this container.
+   *
+   * @param number aScale
+   *        The current waterfall scale.
+   */
+  _showWaterfallDivisionLabels: function NVRM__showWaterfallDivisionLabels(aScale) {
+    let container = $("#requests-menu-waterfall-header-box");
+    let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
+
+    // Nuke all existing labels.
+    while (container.hasChildNodes()) {
+      container.firstChild.remove();
+    }
+
+    // Build new millisecond tick labels...
+    let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
+    let optimalTickIntervalFound = false;
+
+    while (!optimalTickIntervalFound) {
+      // Ignore any divisions that would end up being too close to each other.
+      let scaledStep = aScale * timingStep;
+      if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
+        timingStep <<= 1;
+        continue;
+      }
+      optimalTickIntervalFound = true;
+
+      // Insert one label for each division on the current scale.
+      let fragment = document.createDocumentFragment();
+
+      for (let x = 0; x < availableWidth; x += scaledStep) {
+        let divisionMS = (x / aScale).toFixed(0);
+        let translateX = "translateX(" + (x | 0) + "px)";
+
+        let node = document.createElement("label");
+        let text = L10N.getFormatStr("networkMenu.divisionMS", divisionMS);
+        node.className = "plain requests-menu-timings-division";
+        node.style.transform = translateX;
+
+        node.setAttribute("value", text);
+        fragment.appendChild(node);
+      }
+      container.appendChild(fragment);
+    }
+  },
+
+  /**
+   * Creates the background displayed on each waterfall view in this container.
+   *
+   * @param number aScale
+   *        The current waterfall scale.
+   */
+  _drawWaterfallBackground: function NVRM__drawWaterfallBackground(aScale) {
+    if (!this._canvas || !this._ctx) {
+      this._canvas = document.createElementNS(HTML_NS, "canvas");
+      this._ctx = this._canvas.getContext("2d");
+    }
+    let canvas = this._canvas;
+    let ctx = this._ctx;
+
+    // Nuke the context.
+    let canvasWidth = canvas.width = this._waterfallWidth;
+    let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
+
+    // Start over.
+    let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+    let pixelArray = imageData.data;
+
+    let buf = new ArrayBuffer(pixelArray.length);
+    let buf8 = new Uint8ClampedArray(buf);
+    let data32 = new Uint32Array(buf);
+
+    // Build new millisecond tick lines...
+    let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
+    let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+    let optimalTickIntervalFound = false;
+
+    while (!optimalTickIntervalFound) {
+      // Ignore any divisions that would end up being too close to each other.
+      let scaledStep = aScale * timingStep;
+      if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
+        timingStep <<= 1;
+        continue;
+      }
+      optimalTickIntervalFound = true;
+
+      // Insert one pixel for each division on each scale.
+      for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+        let increment = scaledStep * Math.pow(2, i);
+        for (let x = 0; x < canvasWidth; x += increment) {
+          data32[x | 0] = (alphaComponent << 24) | (255 << 16) | (255 <<  8) | 255;
+        }
+        alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+      }
+    }
+
+    // Flush the image data and cache the waterfall background.
+    pixelArray.set(buf8);
+    ctx.putImageData(imageData, 0, 0);
+    this._cachedWaterfallBackground = "url(" + canvas.toDataURL() + ")";
+  },
+
+  /**
+   * Reapplies the current waterfall background on all request items.
+   */
+  _flushWaterfallBackgrounds: function NVRM__flushWaterfallBackgrounds() {
+    for (let [, { target }] of this._cache) {
+      let waterfallNode = $(".requests-menu-waterfall", target);
+      waterfallNode.style.backgroundImage = this._cachedWaterfallBackground;
+    }
+  },
+
+  /**
+   * Hides the overflowing columns in the requests table.
+   */
+  _hideOverflowingColumns: function NVRM__hideOverflowingColumns() {
+    let table = $("#network-table");
+    let toolbar = $("#requests-menu-toolbar");
+    let columns = [
+      ["#requests-menu-waterfall-header-box", "waterfall-overflows"],
+      ["#requests-menu-size-header-box", "size-overflows"],
+      ["#requests-menu-type-header-box", "type-overflows"],
+      ["#requests-menu-domain-header-box", "domain-overflows"]
+    ];
+
+    // Flush headers.
+    columns.forEach(([, attribute]) => table.removeAttribute(attribute));
+    let availableWidth = toolbar.getBoundingClientRect().width;
+
+    // Hide the columns.
+    columns.forEach(([id, attribute]) => {
+      let bounds = $(id).getBoundingClientRect();
+      if (bounds.right > availableWidth - REQUESTS_HEADERS_SAFE_BOUNDS) {
+        table.setAttribute(attribute, "");
+      }
+    });
+  },
+
+  /**
    * Function called each time a network request item is removed.
    *
    * @param MenuItem aItem
    *        The corresponding menu item.
    */
   _onRequestItemRemoved: function NVRM__onRequestItemRemoved(aItem) {
     dumpn("Finalizing network request item: " + aItem);
     this._cache.delete(aItem.attachment.id);
@@ -680,17 +939,18 @@ create({ constructor: RequestsMenuView, 
     NetMonitorView.NetworkDetails.populate(this.selectedItem.attachment);
     NetMonitorView.NetworkDetails.toggle(true);
   },
 
   /**
    * The resize listener for this container's window.
    */
   _onResize: function NVRM__onResize(e) {
-    this._flushWaterfallViews(true);
+    // Allow requests to settle down first.
+    drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
   },
 
   /**
    * Checks if the specified unix time is the first one to be known of,
    * and saves it if so.
    *
    * @param number aUnixTime
    *        The milliseconds to check and save.
@@ -716,30 +976,34 @@ create({ constructor: RequestsMenuView, 
 
   /**
    * Gets the available waterfall width in this container.
    * @return number
    */
   get _waterfallWidth() {
     if (this._cachedWaterfallWidth == 0) {
       let container = $("#requests-menu-toolbar");
-      let waterfall = $("#requests-menu-waterfall-label");
+      let waterfall = $("#requests-menu-waterfall-header-box");
       let containerBounds = container.getBoundingClientRect();
       let waterfallBounds = waterfall.getBoundingClientRect();
       this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left;
     }
     return this._cachedWaterfallWidth;
   },
 
   _cache: null,
+  _canvas: null,
+  _ctx: null,
   _cachedWaterfallWidth: 0,
+  _cachedWaterfallBackground: null,
   _firstRequestStartedMillis: -1,
   _lastRequestEndedMillis: -1,
   _updateQueue: [],
-  _updateTimeout: null
+  _updateTimeout: null,
+  _resizeTimeout: null
 });
 
 /**
  * Functions handling the requests details view.
  */
 function NetworkDetailsView() {
   dumpn("NetworkDetailsView was instantiated");
 };
@@ -1111,17 +1375,17 @@ create({ constructor: NetworkDetailsView
           $("#response-content-image-dimensions-value").setAttribute("value", dimensions);
         };
       }
       // Handle anything else.
       else {
         $("#response-content-textarea-box").hidden = false;
         NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
           aEditor.setMode(SourceEditor.MODES.TEXT);
-          aEditor.setText(aString);
+          aEditor.setText(NetworkHelper.convertToUnicode(aString, "UTF-8"));
 
           // Maybe set a more appropriate mode in the Source Editor if possible.
           for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
             if (mimeType.contains(key)) {
               aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
               break;
             }
           }
@@ -1209,13 +1473,29 @@ create({ constructor: NetworkDetailsView
   _paramsPostPayload: "",
   _requestHeaders: "",
   _responseHeaders: "",
   _requestCookies: "",
   _responseCookies: ""
 });
 
 /**
+ * DOM query helper.
+ */
+function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
+
+/**
+ * Helper for draining a rapid succession of events and invoking a callback
+ * once everything settles down.
+ */
+function drain(aId, aWait, aCallback) {
+  window.clearTimeout(drain.store.get(aId));
+  drain.store.set(aId, window.setTimeout(aCallback, aWait));
+}
+
+drain.store = new Map();
+
+/**
  * Preliminary setup for the NetMonitorView object.
  */
 NetMonitorView.Toolbar = new ToolbarView();
 NetMonitorView.RequestsMenu = new RequestsMenuView();
 NetMonitorView.NetworkDetails = new NetworkDetailsView();
--- a/browser/devtools/netmonitor/netmonitor.css
+++ b/browser/devtools/netmonitor/netmonitor.css
@@ -1,13 +1,17 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+#details-pane-toggle[disabled] {
+  visibility: hidden;
+}
+
 #response-content-image-box {
   overflow: auto;
 }
 
 #timings-summary-blocked {
   display: none; /* This doesn't work yet. */
 }
 
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -17,53 +17,94 @@
   <script type="text/javascript" src="netmonitor-controller.js"/>
   <script type="text/javascript" src="netmonitor-view.js"/>
 
   <box id="body" flex="1" class="devtools-responsive-container">
     <vbox id="network-table" flex="1">
       <toolbar id="requests-menu-toolbar"
                class="devtools-toolbar"
                align="center">
-        <label id="requests-menu-status-and-method-label"
-               class="plain requests-menu-header requests-menu-status-and-method"
-               value="&netmonitorUI.toolbar.method;"
-               crop="end"/>
-        <label id="requests-menu-file-label"
-               class="plain requests-menu-header requests-menu-file"
-               value="&netmonitorUI.toolbar.file;"
-               crop="end"/>
-        <label id="requests-menu-domain-label"
-               class="plain requests-menu-header requests-menu-domain"
-               value="&netmonitorUI.toolbar.domain;"
-               crop="end"/>
-        <label id="requests-menu-type-label"
-               class="plain requests-menu-header requests-menu-type"
-               value="&netmonitorUI.toolbar.type;"
-               crop="end"/>
-        <label id="requests-menu-size-label"
-               class="plain requests-menu-header requests-menu-size"
-               value="&netmonitorUI.toolbar.size;"
-               crop="end"/>
-        <label id="requests-menu-waterfall-label"
-               class="plain requests-menu-header requests-menu-waterfall"
-               value="&netmonitorUI.toolbar.waterfall;"
-               crop="end"/>
+        <hbox id="toolbar-labels" flex="1">
+          <hbox id="requests-menu-status-and-method-header-box"
+                class="requests-menu-header requests-menu-status-and-method"
+                align="center">
+            <button id="requests-menu-status-button"
+                    class="requests-menu-header-button requests-menu-status"
+                    onclick="NetMonitorView.RequestsMenu.sortBy('status')">
+              &netmonitorUI.toolbar.status;
+            </button>
+            <button id="requests-menu-method-button"
+                    class="requests-menu-header-button requests-menu-method"
+                    onclick="NetMonitorView.RequestsMenu.sortBy('method')"
+                    flex="1">
+              &netmonitorUI.toolbar.method;
+            </button>
+          </hbox>
+          <hbox id="requests-menu-file-header-box"
+                class="requests-menu-header requests-menu-file"
+                align="center">
+            <button id="requests-menu-file-button"
+                    class="requests-menu-header-button requests-menu-file"
+                    onclick="NetMonitorView.RequestsMenu.sortBy('file')"
+                    flex="1">
+              &netmonitorUI.toolbar.file;
+            </button>
+          </hbox>
+          <hbox id="requests-menu-domain-header-box"
+                class="requests-menu-header requests-menu-domain"
+                align="center">
+            <button id="requests-menu-domain-button"
+                    class="requests-menu-header-button requests-menu-domain"
+                    onclick="NetMonitorView.RequestsMenu.sortBy('domain')"
+                    flex="1">
+              &netmonitorUI.toolbar.domain;
+            </button>
+          </hbox>
+          <hbox id="requests-menu-type-header-box"
+                class="requests-menu-header requests-menu-type"
+                align="center">
+            <button id="requests-menu-type-button"
+                    class="requests-menu-header-button requests-menu-type"
+                    onclick="NetMonitorView.RequestsMenu.sortBy('type')"
+                    flex="1">
+              &netmonitorUI.toolbar.type;
+            </button>
+          </hbox>
+          <hbox id="requests-menu-size-header-box"
+                class="requests-menu-header requests-menu-size"
+                align="center">
+            <button id="requests-menu-size-button"
+                    class="requests-menu-header-button requests-menu-size"
+                    onclick="NetMonitorView.RequestsMenu.sortBy('size')"
+                    flex="1">
+              &netmonitorUI.toolbar.size;
+            </button>
+          </hbox>
+          <hbox id="requests-menu-waterfall-header-box"
+                class="requests-menu-header requests-menu-waterfall"
+                align="center">
+            <label id="requests-menu-waterfall-label"
+                   class="plain requests-menu-waterfall"
+                   value="&netmonitorUI.toolbar.waterfall;"/>
+          </hbox>
+        </hbox>
         <spacer id="toolbar-spacer" flex="1"/>
         <toolbarbutton id="details-pane-toggle"
                        class="devtools-toolbarbutton"
                        tooltiptext="&netmonitorUI.panesButton.tooltip;"
+                       disabled="true"
                        tabindex="0"/>
       </toolbar>
       <label class="plain requests-menu-empty-notice"
              value="&netmonitorUI.emptyNotice;"/>
       <vbox id="requests-menu-contents" flex="1">
         <hbox id="requests-menu-item-template" hidden="true">
           <hbox class="requests-menu-subitem requests-menu-status-and-method"
                 align="center">
-            <hbox class="requests-menu-status"/>
+            <box class="requests-menu-status"/>
             <label class="plain requests-menu-method"
                    crop="end"
                    flex="1"/>
           </hbox>
           <label class="plain requests-menu-subitem requests-menu-file"
                  crop="end"/>
           <label class="plain requests-menu-subitem requests-menu-domain"
                  crop="end"/>
@@ -119,17 +160,17 @@
                      crop="end"
                      flex="1"/>
             </hbox>
             <hbox id="headers-summary-status"
                   class="tabpanel-summary-container"
                   align="center">
               <label class="plain tabpanel-summary-label"
                      value="&netmonitorUI.summary.status;"/>
-              <hbox id="headers-summary-status-circle"
+              <box id="headers-summary-status-circle"
                     class="requests-menu-status"/>
               <label id="headers-summary-status-value"
                      class="plain tabpanel-summary-value"
                      crop="end"
                      flex="1"/>
             </hbox>
             <hbox id="headers-summary-version"
                   class="tabpanel-summary-container"
--- a/browser/devtools/netmonitor/test/Makefile.in
+++ b/browser/devtools/netmonitor/test/Makefile.in
@@ -7,41 +7,51 @@ topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_TESTS = \
 	browser_net_aaa_leaktest.js \
+	browser_net_autoscroll.js \
 	browser_net_simple-init.js \
 	browser_net_page-nav.js \
 	browser_net_prefs-and-l10n.js \
 	browser_net_prefs-reload.js \
 	browser_net_pane-collapse.js \
 	browser_net_simple-request.js \
 	browser_net_simple-request-data.js \
 	browser_net_simple-request-details.js \
 	browser_net_content-type.js \
+	browser_net_cyrillic.js \
 	browser_net_status-codes.js \
 	browser_net_post-data.js \
 	browser_net_jsonp.js \
 	browser_net_json-long.js \
+	browser_net_timeline_ticks.js \
+	browser_net_sort-01.js \
+	browser_net_sort-02.js \
+	browser_net_sort-03.js \
 	head.js \
 	$(NULL)
 
 MOCHITEST_BROWSER_PAGES = \
 	test-image.png \
 	html_simple-test-page.html \
 	html_navigate-test-page.html \
 	html_content-type-test-page.html \
+	html_cyrillic-test-page.html \
 	html_status-codes-test-page.html \
 	html_post-data-test-page.html \
 	html_jsonp-test-page.html \
 	html_json-long-test-page.html \
+	html_sorting-test-page.html \
+	html_infinite-get-page.html \
 	sjs_simple-test-server.sjs \
 	sjs_content-type-test-server.sjs \
 	sjs_status-codes-test-server.sjs \
+	sjs_sorting-test-server.sjs \
 	$(NULL)
 
 MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_autoscroll.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 863102 - Automatically scroll down upon new network requests.
+ */
+
+function test() {
+  let monitor, debuggee, requestsContainer, scrollTop;
+
+  initNetMonitor(INFINITE_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    monitor = aMonitor;
+    debuggee = aDebuggee;
+    let win = monitor.panelWin;
+    let topNode = win.document.getElementById("requests-menu-contents");
+    requestsContainer = topNode.getElementsByTagName("scrollbox")[0];
+    ok(!!requestsContainer, "Container element exists as expected.");
+  })
+
+  // (1) Check that the scroll position is maintained at the bottom
+  // when the requests overflow the vertical size of the container.
+  .then(() => {
+    debuggee.performRequests();
+    return waitForRequestsToOverflowContainer(monitor, requestsContainer);
+  }).then(() => {
+    ok(scrolledToBottom(requestsContainer), "Scrolled to bottom on overflow.");
+  })
+
+  // (2) Now set the scroll position somewhere in the middle and check
+  // that additional requests do not change the scroll position.
+  .then(() => {
+    let children = requestsContainer.childNodes;
+    let middleNode = children.item(children.length / 2);
+    middleNode.scrollIntoView();
+    ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom.");
+    scrollTop = requestsContainer.scrollTop; // save for comparison later
+    return waitForNetworkEvents(monitor, 8);
+  }).then(() => {
+    is(requestsContainer.scrollTop, scrollTop, "Did not scroll.");
+  })
+
+  // (3) Now set the scroll position back at the bottom and check that
+  // additional requests *do* cause the container to scroll down.
+  .then(() => {
+    requestsContainer.scrollTop = requestsContainer.scrollHeight;
+    ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom.");
+    return waitForNetworkEvents(monitor, 8);
+  }).then(() => {
+    ok(scrolledToBottom(requestsContainer), "Still scrolled to bottom.");
+  })
+
+  // Done; clean up.
+  .then(() => {
+    return teardown(monitor).then(finish);
+  })
+
+  // Handle exceptions in the chain of promises.
+  .then(null, (err) => {
+    ok(false, err);
+    finish();
+  });
+
+  function waitForRequestsToOverflowContainer (aMonitor, aContainer) {
+    return waitForNetworkEvents(aMonitor, 1).then(() => {
+      if (aContainer.scrollHeight > aContainer.clientHeight) {
+        // Wait for some more just for good measure.
+        return waitForNetworkEvents(aMonitor, 8);
+      } else {
+        return waitForRequestsToOverflowContainer(aMonitor, aContainer);
+      }
+    });
+  }
+
+  function scrolledToBottom(aElement) {
+    return aElement.scrollTop + aElement.clientHeight >= aElement.scrollHeight;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_cyrillic.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if cyrillic text is rendered correctly in the source editor.
+ */
+
+function test() {
+  initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 1).then(() => {
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
+        "GET", CONTENT_TYPE_SJS + "?fmt=txt", {
+          status: 200,
+          statusText: "DA DA DA"
+        });
+
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[3]);
+
+      NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
+        is(aEditor.getText().indexOf("\u044F"), 26, // я
+          "The text shown in the source editor is incorrect.");
+        is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+          "The mode active in the source editor is incorrect.");
+
+        teardown(aMonitor).then(finish);
+      });
+    });
+
+    aDebuggee.performRequests();
+  });
+}
--- a/browser/devtools/netmonitor/test/browser_net_simple-request.js
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request.js
@@ -9,34 +9,43 @@ function test() {
   initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     let { document, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
+    is(document.querySelector("#details-pane-toggle")
+      .hasAttribute("disabled"), true,
+      "The pane toggle button should be disabled when the frontend is opened.");
     is(document.querySelector(".requests-menu-empty-notice")
       .hasAttribute("hidden"), false,
       "An empty notice should be displayed when the frontend is opened.");
     is(RequestsMenu.itemCount, 0,
       "The requests menu should be empty when the frontend is opened.");
     is(NetMonitorView.detailsPaneHidden, true,
       "The details pane should be hidden when the frontend is opened.");
 
     aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+      is(document.querySelector("#details-pane-toggle")
+        .hasAttribute("disabled"), false,
+        "The pane toggle button should be enabled after the first request.");
       is(document.querySelector(".requests-menu-empty-notice")
         .hasAttribute("hidden"), true,
         "The empty notice should be hidden after the first request.");
       is(RequestsMenu.itemCount, 1,
         "The requests menu should not be empty after the first request.");
       is(NetMonitorView.detailsPaneHidden, true,
         "The details pane should still be hidden after the first request.");
 
       aMonitor.panelWin.once("NetMonitor:NetworkEvent", () => {
+        is(document.querySelector("#details-pane-toggle")
+          .hasAttribute("disabled"), false,
+          "The pane toggle button should be still be enabled after a reload.");
         is(document.querySelector(".requests-menu-empty-notice")
           .hasAttribute("hidden"), true,
           "The empty notice should be still hidden after a reload.");
         is(RequestsMenu.itemCount, 1,
           "The requests menu should not be empty after a reload.");
         is(NetMonitorView.detailsPaneHidden, true,
           "The details pane should still be hidden after a reload.");
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_sort-01.js
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if the sorting mechanism works correctly.
+ */
+
+function test() {
+  initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { L10N, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 5).then(() => {
+      testContents([0, 1, 2, 3, 4])
+        .then(() => {
+          info("Testing swap(0, 0)");
+          RequestsMenu.swapItemsAtIndices(0, 0);
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing swap(0, 1)");
+          RequestsMenu.swapItemsAtIndices(0, 1);
+          return testContents([1, 0, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing swap(0, 2)");
+          RequestsMenu.swapItemsAtIndices(0, 2);
+          return testContents([1, 2, 0, 3, 4]);
+        })
+        .then(() => {
+          info("Testing swap(0, 3)");
+          RequestsMenu.swapItemsAtIndices(0, 3);
+          return testContents([1, 2, 3, 0, 4]);
+        })
+        .then(() => {
+          info("Testing swap(0, 4)");
+          RequestsMenu.swapItemsAtIndices(0, 4);
+          return testContents([1, 2, 3, 4, 0]);
+        })
+        .then(() => {
+          info("Testing swap(1, 0)");
+          RequestsMenu.swapItemsAtIndices(1, 0);
+          return testContents([0, 2, 3, 4, 1]);
+        })
+        .then(() => {
+          info("Testing swap(1, 1)");
+          RequestsMenu.swapItemsAtIndices(1, 1);
+          return testContents([0, 2, 3, 4, 1]);
+        })
+        .then(() => {
+          info("Testing swap(1, 2)");
+          RequestsMenu.swapItemsAtIndices(1, 2);
+          return testContents([0, 1, 3, 4, 2]);
+        })
+        .then(() => {
+          info("Testing swap(1, 3)");
+          RequestsMenu.swapItemsAtIndices(1, 3);
+          return testContents([0, 3, 1, 4, 2]);
+        })
+        .then(() => {
+          info("Testing swap(1, 4)");
+          RequestsMenu.swapItemsAtIndices(1, 4);
+          return testContents([0, 3, 4, 1, 2]);
+        })
+        .then(() => {
+          info("Testing swap(2, 0)");
+          RequestsMenu.swapItemsAtIndices(2, 0);
+          return testContents([2, 3, 4, 1, 0]);
+        })
+        .then(() => {
+          info("Testing swap(2, 1)");
+          RequestsMenu.swapItemsAtIndices(2, 1);
+          return testContents([1, 3, 4, 2, 0]);
+        })
+        .then(() => {
+          info("Testing swap(2, 2)");
+          RequestsMenu.swapItemsAtIndices(2, 2);
+          return testContents([1, 3, 4, 2, 0]);
+        })
+        .then(() => {
+          info("Testing swap(2, 3)");
+          RequestsMenu.swapItemsAtIndices(2, 3);
+          return testContents([1, 2, 4, 3, 0]);
+        })
+        .then(() => {
+          info("Testing swap(2, 4)");
+          RequestsMenu.swapItemsAtIndices(2, 4);
+          return testContents([1, 4, 2, 3, 0]);
+        })
+        .then(() => {
+          info("Testing swap(3, 0)");
+          RequestsMenu.swapItemsAtIndices(3, 0);
+          return testContents([1, 4, 2, 0, 3]);
+        })
+        .then(() => {
+          info("Testing swap(3, 1)");
+          RequestsMenu.swapItemsAtIndices(3, 1);
+          return testContents([3, 4, 2, 0, 1]);
+        })
+        .then(() => {
+          info("Testing swap(3, 2)");
+          RequestsMenu.swapItemsAtIndices(3, 2);
+          return testContents([2, 4, 3, 0, 1]);
+        })
+        .then(() => {
+          info("Testing swap(3, 3)");
+          RequestsMenu.swapItemsAtIndices(3, 3);
+          return testContents([2, 4, 3, 0, 1]);
+        })
+        .then(() => {
+          info("Testing swap(3, 4)");
+          RequestsMenu.swapItemsAtIndices(3, 4);
+          return testContents([2, 3, 4, 0, 1]);
+        })
+        .then(() => {
+          info("Testing swap(4, 0)");
+          RequestsMenu.swapItemsAtIndices(4, 0);
+          return testContents([2, 3, 0, 4, 1]);
+        })
+        .then(() => {
+          info("Testing swap(4, 1)");
+          RequestsMenu.swapItemsAtIndices(4, 1);
+          return testContents([2, 3, 0, 1, 4]);
+        })
+        .then(() => {
+          info("Testing swap(4, 2)");
+          RequestsMenu.swapItemsAtIndices(4, 2);
+          return testContents([4, 3, 0, 1, 2]);
+        })
+        .then(() => {
+          info("Testing swap(4, 3)");
+          RequestsMenu.swapItemsAtIndices(4, 3);
+          return testContents([3, 4, 0, 1, 2]);
+        })
+        .then(() => {
+          info("Testing swap(4, 4)");
+          RequestsMenu.swapItemsAtIndices(4, 4);
+          return testContents([3, 4, 0, 1, 2]);
+        })
+        .then(() => {
+          RequestsMenu.sortBy(null);
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          return teardown(aMonitor);
+        })
+        .then(finish);
+    });
+
+    function testContents([a, b, c, d, e]) {
+      let deferred = Promise.defer();
+
+      is(RequestsMenu.allItems.length, 5,
+        "There should be a total of 5 items in the requests menu.");
+      is(RequestsMenu.visibleItems.length, 5,
+        "There should be a total of 5 visbile items in the requests menu.");
+
+      is(RequestsMenu.getItemAtIndex(0), RequestsMenu.allItems[0],
+        "The requests menu items aren't ordered correctly. First item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(1), RequestsMenu.allItems[1],
+        "The requests menu items aren't ordered correctly. Second item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(2), RequestsMenu.allItems[2],
+        "The requests menu items aren't ordered correctly. Third item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(3), RequestsMenu.allItems[3],
+        "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(4), RequestsMenu.allItems[4],
+        "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
+
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+        "GET", STATUS_CODES_SJS + "?sts=100", {
+          status: 101,
+          statusText: "Switching Protocols",
+          type: "plain",
+          fullMimeType: "text/plain; charset=utf-8",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+        "GET", STATUS_CODES_SJS + "?sts=200", {
+          status: 202,
+          statusText: "Created",
+          type: "plain",
+          fullMimeType: "text/plain; charset=utf-8",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.02),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+        "GET", STATUS_CODES_SJS + "?sts=300", {
+          status: 303,
+          statusText: "See Other",
+          type: "plain",
+          fullMimeType: "text/plain; charset=utf-8",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+        "GET", STATUS_CODES_SJS + "?sts=400", {
+          status: 404,
+          statusText: "Not Found",
+          type: "plain",
+          fullMimeType: "text/plain; charset=utf-8",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.02),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+        "GET", STATUS_CODES_SJS + "?sts=500", {
+          status: 501,
+          statusText: "Not Implemented",
+          type: "plain",
+          fullMimeType: "text/plain; charset=utf-8",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.02),
+          time: true
+        });
+
+      executeSoon(deferred.resolve);
+      return deferred.promise;
+    }
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_sort-02.js
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if sorting columns in the network table works correctly.
+ */
+
+function test() {
+  initNetMonitor(SORTING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { $, L10N, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 5).then(() => {
+      EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+      isnot(RequestsMenu.selectedItem, null,
+        "There should be a selected item in the requests menu.");
+      is(RequestsMenu.selectedIndex, 0,
+        "The first item should be selected in the requests menu.");
+      is(NetMonitorView.detailsPaneHidden, false,
+        "The details pane should not be hidden after toggle button was pressed.");
+
+      testHeaders();
+      testContents([0, 2, 4, 3, 1])
+        .then(() => {
+          info("Testing status sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+          testHeaders("status", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing status sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+          testHeaders("status", "descending");
+          return testContents([4, 3, 2, 1, 0]);
+        })
+        .then(() => {
+          info("Clearing status sort.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+          testHeaders();
+          return testContents([0, 2, 4, 3, 1]);
+        })
+        .then(() => {
+          info("Testing method sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+          testHeaders("method", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing method sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+          testHeaders("method", "descending");
+          return testContents([4, 3, 2, 1, 0]);
+        })
+        .then(() => {
+          info("Clearing method sort.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-method-button"));
+          testHeaders();
+          return testContents([0, 2, 4, 3, 1]);
+        })
+        .then(() => {
+          info("Testing file sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+          testHeaders("file", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing file sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+          testHeaders("file", "descending");
+          return testContents([4, 3, 2, 1, 0]);
+        })
+        .then(() => {
+          info("Clearing file sort.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
+          testHeaders();
+          return testContents([0, 2, 4, 3, 1]);
+        })
+        .then(() => {
+          info("Testing type sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+          testHeaders("type", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing type sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+          testHeaders("type", "descending");
+          return testContents([4, 3, 2, 1, 0]);
+        })
+        .then(() => {
+          info("Clearing type sort.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-type-button"));
+          testHeaders();
+          return testContents([0, 2, 4, 3, 1]);
+        })
+        .then(() => {
+          info("Testing size sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+          testHeaders("size", "ascending");
+          return testContents([0, 1, 2, 3, 4]);
+        })
+        .then(() => {
+          info("Testing size sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+          testHeaders("size", "descending");
+          return testContents([4, 3, 2, 1, 0]);
+        })
+        .then(() => {
+          info("Clearing size sort.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-size-button"));
+          testHeaders();
+          return testContents([0, 2, 4, 3, 1]);
+        })
+        .then(() => {
+          return teardown(aMonitor);
+        })
+        .then(finish);
+    });
+
+    function testHeaders(aSortType, aDirection) {
+      let doc = aMonitor.panelWin.document;
+      let target = doc.querySelector("#requests-menu-" + aSortType + "-button");
+      let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+      for (let header of headers) {
+        if (header != target) {
+          is(header.hasAttribute("sorted"), false,
+            "The " + header.id + " header should not have a 'sorted' attribute.");
+          is(header.hasAttribute("tooltiptext"), false,
+            "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+        } else {
+          is(header.getAttribute("sorted"), aDirection,
+            "The " + header.id + " header has an incorrect 'sorted' attribute.");
+          is(header.getAttribute("tooltiptext"), aDirection == "ascending"
+            ? L10N.getStr("networkMenu.sortedAsc")
+            : L10N.getStr("networkMenu.sortedDesc"),
+            "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+        }
+      }
+    }
+
+    function testContents([a, b, c, d, e]) {
+      let deferred = Promise.defer();
+
+      isnot(RequestsMenu.selectedItem, null,
+        "There should still be a selected item after sorting.");
+      is(RequestsMenu.selectedIndex, a,
+        "The first item should be still selected after sorting.");
+      is(NetMonitorView.detailsPaneHidden, false,
+        "The details pane should still be visible after sorting.");
+
+      is(RequestsMenu.allItems.length, 5,
+        "There should be a total of 5 items in the requests menu.");
+      is(RequestsMenu.visibleItems.length, 5,
+        "There should be a total of 5 visbile items in the requests menu.");
+
+      is(RequestsMenu.getItemAtIndex(0), RequestsMenu.allItems[0],
+        "The requests menu items aren't ordered correctly. First item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(1), RequestsMenu.allItems[1],
+        "The requests menu items aren't ordered correctly. Second item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(2), RequestsMenu.allItems[2],
+        "The requests menu items aren't ordered correctly. Third item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(3), RequestsMenu.allItems[3],
+        "The requests menu items aren't ordered correctly. Fourth item is misplaced.");
+      is(RequestsMenu.getItemAtIndex(4), RequestsMenu.allItems[4],
+        "The requests menu items aren't ordered correctly. Fifth item is misplaced.");
+
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(a),
+        "GET1", SORTING_SJS + "?index=1", {
+          status: 101,
+          statusText: "Meh",
+          type: "1",
+          fullMimeType: "text/1",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(b),
+        "GET2", SORTING_SJS + "?index=2", {
+          status: 200,
+          statusText: "Meh",
+          type: "2",
+          fullMimeType: "text/2",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.01),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c),
+        "GET3", SORTING_SJS + "?index=3", {
+          status: 300,
+          statusText: "Meh",
+          type: "3",
+          fullMimeType: "text/3",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.02),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d),
+        "GET4", SORTING_SJS + "?index=4", {
+          status: 400,
+          statusText: "Meh",
+          type: "4",
+          fullMimeType: "text/4",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.03),
+          time: true
+        });
+      verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e),
+        "GET5", SORTING_SJS + "?index=5", {
+          status: 500,
+          statusText: "Meh",
+          type: "5",
+          fullMimeType: "text/5",
+          size: L10N.getFormatStr("networkMenu.sizeKB", 0.04),
+          time: true
+        });
+
+      executeSoon(deferred.resolve);
+      return deferred.promise;
+    }
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_sort-03.js
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test if sorting columns in the network table works correctly with new requests.
+ */
+
+function test() {
+  initNetMonitor(SORTING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { $, L10N, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 5).then(() => {
+      EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
+
+      isnot(RequestsMenu.selectedItem, null,
+        "There should be a selected item in the requests menu.");
+      is(RequestsMenu.selectedIndex, 0,
+        "The first item should be selected in the requests menu.");
+      is(NetMonitorView.detailsPaneHidden, false,
+        "The details pane should not be hidden after toggle button was pressed.");
+
+      testHeaders();
+      testContents([0, 2, 4, 3, 1], 0)
+        .then(() => {
+          info("Testing status sort, ascending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+          testHeaders("status", "ascending");
+          return testContents([0, 1, 2, 3, 4], 0);
+        })
+        .then(() => {
+          info("Performing more requests.");
+          aDebuggee.performRequests();
+          return waitForNetworkEvents(aMonitor, 5);
+        })
+        .then(() => {
+          info("Testing status sort again, ascending.");
+          testHeaders("status", "ascending");
+          return testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
+        })
+        .then(() => {
+          info("Testing status sort, descending.");
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-status-button"));
+          testHeaders("status", "descending");
+          return testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9);
+        })
+        .then(() => {
+          info("Performing more requests.");
+          aDebuggee.performRequests();
+          return waitForNetworkEvents(aMonitor, 5);
+        })
+        .then(() => {
+          info("Testing status sort again, descending.");
+          testHeaders("status", "descending");
+          return testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 12);
+        })
+        .then(() => {
+          return teardown(aMonitor);
+        })
+        .then(finish);
+    });
+
+    function testHeaders(aSortType, aDirection) {
+      let doc = aMonitor.panelWin.document;
+      let target = doc.querySelector("#requests-menu-" + aSortType + "-button");
+      let headers = doc.querySelectorAll(".requests-menu-header-button");
+
+      for (let header of headers) {
+        if (header != target) {
+          is(header.hasAttribute("sorted"), false,
+            "The " + header.id + " header should not have a 'sorted' attribute.");
+          is(header.hasAttribute("tooltiptext"), false,
+            "The " + header.id + " header should not have a 'tooltiptext' attribute.");
+        } else {
+          is(header.getAttribute("sorted"), aDirection,
+            "The " + header.id + " header has an incorrect 'sorted' attribute.");
+          is(header.getAttribute("tooltiptext"), aDirection == "ascending"
+            ? L10N.getStr("networkMenu.sortedAsc")
+            : L10N.getStr("networkMenu.sortedDesc"),
+            "The " + header.id + " has an incorrect 'tooltiptext' attribute.");
+        }
+      }
+    }
+
+    function testContents(aOrder, aSelection) {
+      let deferred = Promise.defer();
+
+      isnot(RequestsMenu.selectedItem, null,
+        "There should still be a selected item after sorting.");
+      is(RequestsMenu.selectedIndex, aSelection,
+        "The first item should be still selected after sorting.");
+      is(NetMonitorView.detailsPaneHidden, false,
+        "The details pane should still be visible after sorting.");
+
+      is(RequestsMenu.allItems.length, aOrder.length,
+        "There should be a specific number of items in the requests menu.");
+      is(RequestsMenu.visibleItems.length, aOrder.length,
+        "There should be a specific number of visbile items in the requests menu.");
+
+      for (let i = 0; i < aOrder.length; i++) {
+        is(RequestsMenu.getItemAtIndex(i), RequestsMenu.allItems[i],
+          "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
+      }
+
+      for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+        verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i]),
+          "GET1", SORTING_SJS + "?index=1", {
+            status: 101,
+            statusText: "Meh",
+            type: "1",
+            fullMimeType: "text/1",
+            size: L10N.getFormatStr("networkMenu.sizeKB", 0),
+            time: true
+          });
+      }
+      for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+        verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len]),
+          "GET2", SORTING_SJS + "?index=2", {
+            status: 200,
+            statusText: "Meh",
+            type: "2",
+            fullMimeType: "text/2",
+            size: L10N.getFormatStr("networkMenu.sizeKB", 0.01),
+            time: true
+          });
+      }
+      for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+        verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 2]),
+          "GET3", SORTING_SJS + "?index=3", {
+            status: 300,
+            statusText: "Meh",
+            type: "3",
+            fullMimeType: "text/3",
+            size: L10N.getFormatStr("networkMenu.sizeKB", 0.02),
+            time: true
+          });
+      }
+      for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+        verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 3]),
+          "GET4", SORTING_SJS + "?index=4", {
+            status: 400,
+            statusText: "Meh",
+            type: "4",
+            fullMimeType: "text/4",
+            size: L10N.getFormatStr("networkMenu.sizeKB", 0.03),
+            time: true
+          });
+      }
+      for (let i = 0, len = aOrder.length / 5; i < len; i++) {
+        verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i + len * 4]),
+          "GET5", SORTING_SJS + "?index=5", {
+            status: 500,
+            statusText: "Meh",
+            type: "5",
+            fullMimeType: "text/5",
+            size: L10N.getFormatStr("networkMenu.sizeKB", 0.04),
+            time: true
+          });
+      }
+
+      executeSoon(deferred.resolve);
+      return deferred.promise;
+    }
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_timeline_ticks.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if timeline correctly displays interval divisions.
+ */
+
+function test() {
+  initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    is(document.querySelector(".requests-menu-empty-notice")
+      .hasAttribute("hidden"), false,
+      "An timeline label should be displayed when the frontend is opened.");
+    ok(document.querySelectorAll(".requests-menu-timings-division").length == 0,
+      "No tick labels should be displayed when the frontend is opened.");
+
+    ok(!RequestsMenu._canvas,
+      "No canvas should be created when the frontend is opened.");
+    ok(!RequestsMenu._ctx,
+      "No 2d context should be created when the frontend is opened.");
+
+    waitForNetworkEvents(aMonitor, 1).then(() => {
+      is(document.querySelector(".requests-menu-empty-notice")
+        .hasAttribute("hidden"), true,
+        "The timeline label should be hidden after the first request.");
+      ok(document.querySelectorAll(".requests-menu-timings-division").length >= 3,
+        "There should be at least 3 tick labels in the network requests header.");
+
+      is(document.querySelectorAll(".requests-menu-timings-division")[0]
+        .getAttribute("value"), L10N.getFormatStr("networkMenu.divisionMS", 0),
+        "The first tick label has an incorrect value");
+      is(document.querySelectorAll(".requests-menu-timings-division")[1]
+        .getAttribute("value"), L10N.getFormatStr("networkMenu.divisionMS", 80),
+        "The second tick label has an incorrect value");
+      is(document.querySelectorAll(".requests-menu-timings-division")[2]
+        .getAttribute("value"), L10N.getFormatStr("networkMenu.divisionMS", 160),
+        "The third tick label has an incorrect value");
+
+      is(document.querySelectorAll(".requests-menu-timings-division")[0]
+        .style.transform, "translateX(0px)",
+        "The first tick label has an incorrect translation");
+      is(document.querySelectorAll(".requests-menu-timings-division")[1]
+        .style.transform, "translateX(80px)",
+        "The second tick label has an incorrect translation");
+      is(document.querySelectorAll(".requests-menu-timings-division")[2]
+        .style.transform, "translateX(160px)",
+        "The third tick label has an incorrect translation");
+
+      ok(RequestsMenu._canvas,
+        "A canvas should be created after the first request.");
+      ok(RequestsMenu._ctx,
+        "A 2d context should be created after the first request.");
+
+      let imageData = RequestsMenu._ctx.getImageData(0, 0, 161, 1);
+      ok(imageData, "The image data should have been created.");
+
+      let data = imageData.data;
+      ok(data, "The image data should contain a pixel array.");
+
+      ok( hasPixelAt(0), "The tick at 0 is should not be empty.");
+      ok(!hasPixelAt(1), "The tick at 1 is should be empty.");
+      ok(!hasPixelAt(19), "The tick at 19 is should be empty.");
+      ok( hasPixelAt(20), "The tick at 20 is should not be empty.");
+      ok(!hasPixelAt(21), "The tick at 21 is should be empty.");
+      ok(!hasPixelAt(39), "The tick at 39 is should be empty.");
+      ok( hasPixelAt(40), "The tick at 40 is should not be empty.");
+      ok(!hasPixelAt(41), "The tick at 41 is should be empty.");
+      ok(!hasPixelAt(59), "The tick at 59 is should be empty.");
+      ok( hasPixelAt(60), "The tick at 60 is should not be empty.");
+      ok(!hasPixelAt(61), "The tick at 61 is should be empty.");
+      ok(!hasPixelAt(79), "The tick at 79 is should be empty.");
+      ok( hasPixelAt(80), "The tick at 80 is should not be empty.");
+      ok(!hasPixelAt(81), "The tick at 81 is should be empty.");
+      ok(!hasPixelAt(159), "The tick at 159 is should be empty.");
+      ok( hasPixelAt(160), "The tick at 160 is should not be empty.");
+      ok(!hasPixelAt(161), "The tick at 161 is should be empty.");
+
+      ok(isPixelBrighterAtThan(0, 20),
+        "The tick at 0 should be brighter than the one at 20");
+      ok(isPixelBrighterAtThan(40, 20),
+        "The tick at 40 should be brighter than the one at 20");
+      ok(isPixelBrighterAtThan(40, 60),
+        "The tick at 40 should be brighter than the one at 60");
+      ok(isPixelBrighterAtThan(80, 60),
+        "The tick at 80 should be brighter than the one at 60");
+
+      ok(isPixelBrighterAtThan(80, 100),
+        "The tick at 80 should be brighter than the one at 100");
+      ok(isPixelBrighterAtThan(120, 100),
+        "The tick at 120 should be brighter than the one at 100");
+      ok(isPixelBrighterAtThan(120, 140),
+        "The tick at 120 should be brighter than the one at 140");
+      ok(isPixelBrighterAtThan(160, 140),
+        "The tick at 160 should be brighter than the one at 140");
+
+      ok(isPixelEquallyBright(20, 60),
+        "The tick at 20 should be equally bright to the one at 60");
+      ok(isPixelEquallyBright(100, 140),
+        "The tick at 100 should be equally bright to the one at 140");
+
+      ok(isPixelEquallyBright(40, 120),
+        "The tick at 40 should be equally bright to the one at 120");
+
+      ok(isPixelEquallyBright(0, 80),
+        "The tick at 80 should be equally bright to the one at 160");
+      ok(isPixelEquallyBright(80, 160),
+        "The tick at 80 should be equally bright to the one at 160");
+
+      function hasPixelAt(x) {
+        let i = (x | 0) * 4;
+        return data[i] && data[i + 1] && data[i + 2] && data[i + 3];
+      }
+
+      function isPixelBrighterAtThan(x1, x2) {
+        let i = (x1 | 0) * 4;
+        let j = (x2 | 0) * 4;
+        return data[i + 3] > data [j + 3];
+      }
+
+      function isPixelEquallyBright(x1, x2) {
+        let i = (x1 | 0) * 4;
+        let j = (x2 | 0) * 4;
+        return data[i + 3] == data [j + 3];
+      }
+
+      teardown(aMonitor).then(finish);
+    });
+
+    aDebuggee.location.reload();
+  });
+}
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -10,24 +10,28 @@ let { gDevTools, devtools } = Cu.import(
 let TargetFactory = devtools.TargetFactory;
 let Toolbox = devtools.Toolbox;
 
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/netmonitor/test/";
 
 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
 const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
+const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
+const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
+const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
+const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 // Enable logging for all the relevant tests.
 let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_cyrillic-test-page.html
@@ -0,0 +1,33 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Cyrillic type test</p>
+
+    <script type="text/javascript">
+      function get(aAddress, aCallback) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", aAddress, true);
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(null);
+      }
+
+      function performRequests() {
+        get("sjs_content-type-test-server.sjs?fmt=txt", function() {
+          // Done.
+        });
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_infinite-get-page.html
@@ -0,0 +1,36 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Infinite GETs</p>
+
+    <script type="text/javascript">
+      function get(aAddress, aCallback) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", aAddress, true);
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(null);
+      }
+
+      // Use a count parameter to defeat caching.
+      var count = 0;
+
+      function performRequests() {
+        get("request_" + (count++), function() {
+          setTimeout(performRequests, 0);
+        });
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_sorting-test-page.html
@@ -0,0 +1,41 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Sorting test</p>
+
+    <script type="text/javascript">
+      function get(aAddress, aIndex, aCallback) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET" + aIndex, aAddress + "?index=" + aIndex, true);
+
+        xhr.onreadystatechange = function() {
+          if (this.readyState == this.DONE) {
+            aCallback();
+          }
+        };
+        xhr.send(null);
+      }
+
+      function performRequests() {
+        get("sjs_sorting-test-server.sjs", 1, function() {
+          get("sjs_sorting-test-server.sjs", 5, function() {
+            get("sjs_sorting-test-server.sjs", 2, function() {
+              get("sjs_sorting-test-server.sjs", 4, function() {
+                get("sjs_sorting-test-server.sjs", 3, function() {
+                  // Done.
+                });
+              });
+            });
+          });
+        });
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
+++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs
@@ -6,16 +6,23 @@ const { classes: Cc, interfaces: Ci } = 
 function handleRequest(request, response) {
   response.processAsync();
 
   let params = request.queryString.split("&");
   let format = params.filter((s) => s.contains("fmt="))[0].split("=")[1];
 
   Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
     switch (format) {
+      case "txt": {
+        response.setStatusLine(request.httpVersion, 200, "DA DA DA");
+        response.setHeader("Content-Type", "text/plain", false);
+        response.write("Братан, ты вообще качаешься?");
+        response.finish();
+        break;
+      }
       case "xml": {
         response.setStatusLine(request.httpVersion, 200, "OK");
         response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
         response.write("<label value='greeting'>Hello XML!</label>");
         response.finish();
         break;
       }
       case "css": {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/sjs_sorting-test-server.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+function handleRequest(request, response) {
+  response.processAsync();
+
+  let params = request.queryString.split("&");
+  let index = params.filter((s) => s.contains("index="))[0].split("=")[1];
+
+  Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
+    response.setStatusLine(request.httpVersion, index == 1 ? 101 : index * 100, "Meh");
+    response.setHeader("Content-Type", "text/" + index, false);
+    response.write(new Array(index * 10).join(index)); // + 0.01 KB
+    response.finish();
+  }, 50, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
+}
--- a/browser/devtools/profiler/cmd-profiler.jsm
+++ b/browser/devtools/profiler/cmd-profiler.jsm
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 this.EXPORTED_SYMBOLS = [];
 
-Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/Require.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
   "resource:///modules/devtools/gDevTools.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
@@ -75,17 +75,17 @@ gcli.addCommand({
       let panel = getPanel(context, "jsprofiler");
       let profile = panel.getProfileByName(name) || panel.createProfile(name);
 
       if (profile.isStarted) {
         throw gcli.lookup("profilerAlreadyStarted");
       }
 
       if (profile.isFinished) {
-        throw gcli.lookup("profilerAlradyFinished");
+        throw gcli.lookup("profilerAlreadyFinished");
       }
 
       panel.switchToProfile(profile, function () profile.start());
       return gcli.lookup("profilerStarting2");
     }
 
     return gDevTools.showToolbox(context.environment.target, "jsprofiler")
       .then(start);
--- a/browser/devtools/profiler/test/browser_profiler_cmd.js
+++ b/browser/devtools/profiler/test/browser_profiler_cmd.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
 
-let gcli = Cu.import("resource:///modules/devtools/gcli.jsm", {}).gcli;
+let gcli = Cu.import("resource://gre/modules/devtools/gcli.jsm", {}).gcli;
 let gTarget, gPanel, gOptions;
 
 function cmd(typed, expected="") {
   helpers.audit(gOptions, [{
     setup: typed,
     exec: { output: expected }
   }]);
 }
--- a/browser/devtools/responsivedesign/CmdResize.jsm
+++ b/browser/devtools/responsivedesign/CmdResize.jsm
@@ -6,17 +6,17 @@ const { classes: Cc, interfaces: Ci, uti
 
 const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"].
                          getService(Ci.nsIStringBundleService).
                          createBundle("chrome://branding/locale/brand.properties").
                          GetStringFromName("brandShortName");
 
 this.EXPORTED_SYMBOLS = [ ];
 
-Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/devtools/gcli.jsm");
 
 /* Responsive Mode commands */
 gcli.addCommand({
   name: 'resize',
   description: gcli.lookup('resizeModeDesc')
 });
 
 gcli.addCommand({
--- a/browser/devtools/responsivedesign/test/helpers.js
+++ b/browser/devtools/responsivedesign/test/helpers.js
@@ -16,21 +16,20 @@
 //   };
 // Now GCLI will emit output on every keypress that both explains the state
 // of GCLI and can be run as a test case.
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
-Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/scratchpad/CmdScratchpad.jsm
+++ b/browser/devtools/scratchpad/CmdScratchpad.jsm
@@ -1,15 +1,15 @@
 /* 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/. */
 
 this.EXPORTED_SYMBOLS = [ ];
 
-Components.utils.import("resource:///modules/devtools/gcli.jsm");
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm");
 
 /**
  * 'scratchpad' command
  */
 gcli.addCommand({
   name: "scratchpad",
   buttonId: "command-button-scratchpad",
   buttonClass: "command-button",
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -16,17 +16,16 @@
 
 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");
 Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource:///modules/PropertyPanel.jsm");
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
@@ -74,17 +73,17 @@ var Scratchpad = {
       return obj;
     }
 
     aLine = aLine
       .replace(/^\/\//, "")
       .replace(/^\/\*/, "")
       .replace(/\*\/$/, "");
 
-    aLine.split(",").forEach(function (pair) {
+    aLine.split(",").forEach(pair => {
       let [key, val] = pair.split(":");
 
       if (key && val) {
         obj[key.trim()] = val.trim();
       }
     });
 
     return obj;
@@ -616,28 +615,28 @@ var Scratchpad = {
         !window.confirm(this.strings.
                         GetStringFromName("export.fileOverwriteConfirmation"))) {
       return;
     }
 
     let encoder = new TextEncoder();
     let buffer = encoder.encode(this.getText());
     let promise = OS.File.writeAtomic(aFile.path, buffer,{tmpPath: aFile.path + ".tmp"});
-    promise.then(function success(value) {
+    promise.then(value => {
       if (aCallback) {
         aCallback.call(this, Components.results.NS_OK);
       }
-    }.bind(this), function failure(reason) {
+    }, reason => {
       if (!aSilentError) {
         window.alert(this.strings.GetStringFromName("saveFile.failed"));
       }
       if (aCallback) {
         aCallback.call(this, Components.results.NS_ERROR_UNEXPECTED);
       }
-    }.bind(this));
+    });
 
   },
 
   /**
    * Read the content of a file and put it into the textbox.
    *
    * @param nsILocalFile aFile
    *        The file you want to save the textbox content into.
@@ -651,60 +650,59 @@ var Scratchpad = {
    *        2) the data that was read from the file, if any.
    */
   importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
   {
     // Prevent file type detection.
     let channel = NetUtil.newChannel(aFile);
     channel.contentType = "application/javascript";
 
-    let self = this;
-    NetUtil.asyncFetch(channel, function(aInputStream, aStatus) {
+    NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
       let content = null;
 
       if (Components.isSuccessCode(aStatus)) {
         let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                         createInstance(Ci.nsIScriptableUnicodeConverter);
         converter.charset = "UTF-8";
         content = NetUtil.readInputStreamToString(aInputStream,
                                                   aInputStream.available());
         content = converter.ConvertToUnicode(content);
 
         // Check to see if the first line is a mode-line comment.
         let line = content.split("\n")[0];
-        let modeline = self._scanModeLine(line);
+        let modeline = this._scanModeLine(line);
         let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
 
         if (chrome && modeline["-sp-context"] === "browser") {
-          self.setBrowserContext();
+          this.setBrowserContext();
         }
 
-        self.setText(content);
-        self.editor.resetUndo();
+        this.setText(content);
+        this.editor.resetUndo();
       }
       else if (!aSilentError) {
-        window.alert(self.strings.GetStringFromName("openFile.failed"));
+        window.alert(this.strings.GetStringFromName("openFile.failed"));
       }
 
       if (aCallback) {
-        aCallback.call(self, aStatus, content);
+        aCallback.call(this, aStatus, content);
       }
     });
   },
 
   /**
    * Open a file to edit in the Scratchpad.
    *
    * @param integer aIndex
    *        Optional integer: clicked menuitem in the 'Open Recent'-menu.
    */
   openFile: function SP_openFile(aIndex)
   {
-    let promptCallback = function(aFile) {
-      this.promptSave(function(aCloseFile, aSaved, aStatus) {
+    let promptCallback = aFile => {
+      this.promptSave((aCloseFile, aSaved, aStatus) => {
         let shouldOpen = aCloseFile;
         if (aSaved && !Components.isSuccessCode(aStatus)) {
           shouldOpen = false;
         }
 
         if (shouldOpen) {
           let file;
           if (aFile) {
@@ -727,33 +725,31 @@ var Scratchpad = {
             this.clearFiles(aIndex, 1);
             return;
           }
 
           this.setFilename(file.path);
           this.importFromFile(file, false);
           this.setRecentFile(file);
         }
-      }.bind(this));
-    }.bind(this);
+      });
+    };
 
     if (aIndex > -1) {
       promptCallback();
     } else {
       let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-      let fpCallback = function fpCallback_done(aResult) {
+      fp.init(window, this.strings.GetStringFromName("openFile.title"),
+              Ci.nsIFilePicker.modeOpen);
+      fp.defaultString = "";
+      fp.open(aResult => {
         if (aResult != Ci.nsIFilePicker.returnCancel) {
           promptCallback(fp.file);
         }
-      };
-
-      fp.init(window, this.strings.GetStringFromName("openFile.title"),
-              Ci.nsIFilePicker.modeOpen);
-      fp.defaultString = "";
-      fp.open(fpCallback);
+      });
     }
   },
 
   /**
    * Get recent files.
    *
    * @return Array
    *         File paths.
@@ -948,17 +944,17 @@ var Scratchpad = {
   {
     if (!this.filename) {
       return this.saveFileAs(aCallback);
     }
 
     let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
     file.initWithPath(this.filename);
 
-    this.exportToFile(file, true, false, function(aStatus) {
+    this.exportToFile(file, true, false, aStatus => {
       if (Components.isSuccessCode(aStatus)) {
         this.editor.dirty = false;
         this.setRecentFile(file);
       }
       if (aCallback) {
         aCallback(aStatus);
       }
     });
@@ -968,30 +964,30 @@ var Scratchpad = {
    * Save the textbox content to a new file.
    *
    * @param function aCallback
    *        Optional function you want to call when file is saved
    */
   saveFileAs: function SP_saveFileAs(aCallback)
   {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-    let fpCallback = function fpCallback_done(aResult) {
+    let fpCallback = aResult => {
       if (aResult != Ci.nsIFilePicker.returnCancel) {
         this.setFilename(fp.file.path);
-        this.exportToFile(fp.file, true, false, function(aStatus) {
+        this.exportToFile(fp.file, true, false, aStatus => {
           if (Components.isSuccessCode(aStatus)) {
             this.editor.dirty = false;
             this.setRecentFile(fp.file);
           }
           if (aCallback) {
             aCallback(aStatus);
           }
         });
       }
-    }.bind(this);
+    };
 
     fp.init(window, this.strings.GetStringFromName("saveFileAs"),
             Ci.nsIFilePicker.modeSave);
     fp.defaultString = "scratchpad.js";
     fp.open(fpCallback);
   },
 
   /**
@@ -1004,17 +1000,17 @@ var Scratchpad = {
   {
     let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
     file.initWithPath(this.filename);
 
     if (!file.exists()) {
       return;
     }
 
-    this.importFromFile(file, false, function(aStatus, aContent) {
+    this.importFromFile(file, false, (aStatus, aContent) => {
       if (aCallback) {
         aCallback(aStatus);
       }
     });
   },
 
   /**
    * Prompt to revert scratchpad if it has unsaved changes.
@@ -1040,18 +1036,18 @@ var Scratchpad = {
       if (button == BUTTON_POSITION_CANCEL) {
         if (aCallback) {
           aCallback(false);
         }
 
         return;
       }
       if (button == BUTTON_POSITION_REVERT) {
-        this.revertFile(function(aStatus) {
-          if(aCallback){
+        this.revertFile(aStatus => {
+          if (aCallback) {
             aCallback(true, aStatus);
           }
         });
 
         return;
       }
     }
     if (aCallback) {
@@ -1343,17 +1339,17 @@ var Scratchpad = {
       if (button == BUTTON_POSITION_CANCEL) {
         if (aCallback) {
           aCallback(false, false);
         }
         return false;
       }
 
       if (button == BUTTON_POSITION_SAVE) {
-        this.saveFile(function(aStatus) {
+        this.saveFile(aStatus => {
           if (aCallback) {
             aCallback(true, true, aStatus);
           }
         });
         return true;
       }
     }
 
@@ -1382,29 +1378,29 @@ var Scratchpad = {
    * Close the scratchpad window. Prompts before closing if the scratchpad
    * has unsaved changes.
    *
    * @param function aCallback
    *        Optional function you want to call when file is saved
    */
   close: function SP_close(aCallback)
   {
-    this.promptSave(function(aShouldClose, aSaved, aStatus) {
+    this.promptSave((aShouldClose, aSaved, aStatus) => {
       let shouldClose = aShouldClose;
       if (aSaved && !Components.isSuccessCode(aStatus)) {
         shouldClose = false;
       }
 
       if (shouldClose) {
         window.close();
       }
       if (aCallback) {
         aCallback();
       }
-    }.bind(this));
+    });
   },
 
   _observers: [],
 
   /**
    * Add an observer for Scratchpad events.
    *
    * The observer implements IScratchpadObserver := {
--- a/browser/devtools/scratchpad/scratchpad.xul
+++ b/browser/devtools/scratchpad/scratchpad.xul
@@ -48,16 +48,17 @@
   <command id="sp-cmd-display" oncommand="Scratchpad.display();"/>
   <command id="sp-cmd-contentContext" oncommand="Scratchpad.setContentContext();"/>
   <command id="sp-cmd-browserContext" oncommand="Scratchpad.setBrowserContext();" disabled="true"/>
   <command id="sp-cmd-reloadAndRun" oncommand="Scratchpad.reloadAndRun();"/>
   <command id="sp-cmd-resetContext" oncommand="Scratchpad.resetContext();"/>
   <command id="sp-cmd-errorConsole" oncommand="Scratchpad.openErrorConsole();" disabled="true"/>
   <command id="sp-cmd-webConsole" oncommand="Scratchpad.openWebConsole();"/>
   <command id="sp-cmd-documentationLink" oncommand="Scratchpad.openDocumentationPage();"/>
+  <command id="sp-cmd-hideSidebar" oncommand="Scratchpad.sidebar.hide();"/>
 </commandset>
 
 <keyset id="sourceEditorKeys"/>
 
 <keyset id="sp-keyset">
   <key id="sp-key-window"
        key="&newWindowCmd.commandkey;"
        command="sp-cmd-newWindow"
@@ -97,16 +98,19 @@
   <key id="sp-key-reloadAndRun"
        key="&reloadAndRun.key;"
        command="sp-cmd-reloadAndRun"
        modifiers="accel,shift"/>
   <key id="sp-key-errorConsole"
        key="&errorConsoleCmd.commandkey;"
        command="sp-cmd-errorConsole"
        modifiers="accel,shift"/>
+  <key id="sp-key-hideSidebar"
+       keycode="VK_ESCAPE"
+       command="sp-cmd-hideSidebar"/>
   <key id="key_openHelp"
        keycode="VK_F1"
        command="sp-cmd-documentationLink"/>
 </keyset>
 
 
 <menubar id="sp-menubar">
   <menu id="sp-file-menu" label="&fileMenu.label;"
--- a/browser/devtools/shared/AppCacheUtils.jsm
+++ b/browser/devtools/shared/AppCacheUtils.jsm
@@ -236,16 +236,20 @@ AppCacheUtils.prototype = {
           });
         }
       }
     }, null);
     return deferred.promise;
   },
 
   listEntries: function ACU_show(searchTerm) {
+    if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
+      throw new Error(l10n.GetStringFromName("cacheDisabled"));
+    }
+
     let entries = [];
 
     Services.cache.visitEntries({
       visitDevice: function(deviceID, deviceInfo) {
         return true;
       },
 
       visitEntry: function(deviceID, entryInfo) {
@@ -270,16 +274,19 @@ AppCacheUtils.prototype = {
             entry[key] = value;
           }
           entries.push(entry);
         }
         return true;
       }
     });
 
+    if (entries.length == 0) {
+      throw new Error(l10n.GetStringFromName("noResults"));
+    }
     return entries;
   },
 
   viewEntry: function ACU_viewEntry(key) {
     let uri;
 
     Services.cache.visitEntries({
       visitDevice: function(deviceID, deviceInfo) {
@@ -331,17 +338,17 @@ AppCacheUtils.prototype = {
           let html = uriInfo.text;
           let parser = _DOMParser;
           this.doc = parser.parseFromString(html, "text/html");
           let uri = getURI(this.doc);
           deferred.resolve(uri);
         } else {
           this.errors.push({
             line: 0,
-            msg: "The URI passed to AppCacheUtils is invalid."
+            msg: l10n.GetStringFromName("invalidURI")
           });
         }
       });
     }
     return deferred.promise;
   },
 
   _addError: function ACU__addError(line, l10nString, ...params) {
deleted file mode 100644
--- a/browser/devtools/shared/Browser.jsm
+++ /dev/null
@@ -1,28 +0,0 @@
-/* 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";
-
-/**
- * Define various constants to match the globals provided by the browser.
- * This module helps cases where code is shared between the web and Firefox.
- * See also Console.jsm for an implementation of the Firefox console that
- * forwards to dump();
- */
-
-this.EXPORTED_SYMBOLS = [ "Node", "HTMLElement", "setTimeout", "clearTimeout" ];
-
-/**
- * Expose Node/HTMLElement objects. This allows us to use the Node constants
- * without resorting to hardcoded numbers
- */
-this.Node = Components.interfaces.nsIDOMNode;
-this.HTMLElement = Components.interfaces.nsIDOMHTMLElement;
-
-/*
- * Import and re-export the timeout functions from Timer.jsm.
- */
-let Timer = Components.utils.import("resource://gre/modules/Timer.jsm", {});
-this.setTimeout = Timer.setTimeout;
-this.clearTimeout = Timer.clearTimeout;
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -14,17 +14,17 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource:///modules/devtools/Commands.jsm");
 
 const Node = Components.interfaces.nsIDOMNode;
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/devtools/Console.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gcli",
-                                  "resource:///modules/devtools/gcli.jsm");
+                                  "resource://gre/modules/devtools/gcli.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CmdCommands",
                                   "resource:///modules/devtools/BuiltinCommands.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener",
                                   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
@@ -148,17 +148,17 @@ let CommandUtils = {
 
   /**
    * A helper function to create the environment object that is passed to
    * GCLI commands.
    */
   createEnvironment: function(chromeDocument, contentDocument) {
     let environment = {
       chromeDocument: chromeDocument,
-      contentDocument: contentDocument, // Use of contentDocument is deprecated
+      chromeWindow: chromeDocument.defaultView,
 
       document: contentDocument,
       window: contentDocument.defaultView
     };
 
     Object.defineProperty(environment, "target", {
       get: function() {
         let tab = chromeDocument.defaultView.getBrowser().selectedTab;
@@ -176,19 +176,23 @@ this.CommandUtils = CommandUtils;
 /**
  * Due to a number of panel bugs we need a way to check if we are running on
  * Linux. See the comments for TooltipPanel and OutputPanel for further details.
  *
  * When bug 780102 is fixed all isLinux checks can be removed and we can revert
  * to using panels.
  */
 XPCOMUtils.defineLazyGetter(this, "isLinux", function () {
+  return OS == "Linux";
+});
+
+XPCOMUtils.defineLazyGetter(this, "OS", function () {
   let os = Components.classes["@mozilla.org/xre/app-info;1"]
            .getService(Components.interfaces.nsIXULRuntime).OS;
-  return os == "Linux";
+  return os;
 });
 
 /**
  * A component to manage the global developer toolbar, which contains a GCLI
  * and buttons for various developer tools.
  * @param aChromeWindow The browser window to which this toolbar is attached
  * @param aToolbarElement See browser.xul:<toolbar id="developer-toolbar">
  */
@@ -756,16 +760,17 @@ function OutputPanel(aDevToolbar, aLoadC
   this._frame.id = "gcli-output-frame";
   this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml");
   this._frame.setAttribute("sandbox", "allow-same-origin");
   this._panel.appendChild(this._frame);
 
   this.displayedOutput = undefined;
 
   this._onload = this._onload.bind(this);
+  this._update = this._update.bind(this);
   this._frame.addEventListener("load", this._onload, true);
 
   this.loaded = false;
 }
 
 /**
  * Wire up the element from the iframe, and inform the _loadCallback.
  */
@@ -787,43 +792,16 @@ OutputPanel.prototype._onload = function
   this.loaded = true;
   if (this._loadCallback) {
     this._loadCallback();
     delete this._loadCallback;
   }
 };
 
 /**
- * Determine the scrollbar width in the current document.
- *
- * @private
- */
-Object.defineProperty(OutputPanel.prototype, 'scrollbarWidth', {
-  get: function() {
-    if (this.__scrollbarWidth) {
-      return this.__scrollbarWidth;
-    }
-
-    let hbox = this.document.createElementNS(XUL_NS, "hbox");
-    hbox.setAttribute("style", "height: 0%; overflow: hidden");
-
-    let scrollbar = this.document.createElementNS(XUL_NS, "scrollbar");
-    scrollbar.setAttribute("orient", "vertical");
-    hbox.appendChild(scrollbar);
-
-    this.document.documentElement.appendChild(hbox);
-    this.__scrollbarWidth = scrollbar.clientWidth;
-    this.document.documentElement.removeChild(hbox);
-
-    return this.__scrollbarWidth;
-  },
-  enumerable: true
-});
-
-/**
  * Prevent the popup from hiding if it is not permitted via this.canHide.
  */
 OutputPanel.prototype._onpopuphiding = function OP_onpopuphiding(aEvent)
 {
   // TODO: When we switch back from tooltip to panel we can remove this hack:
   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   if (isLinux && !this.canHide) {
     aEvent.preventDefault();
@@ -858,23 +836,42 @@ OutputPanel.prototype._resize = function
 {
   if (this._panel == null || this.document == null || !this._panel.state == "closed") {
     return
   }
 
   // Set max panel width to match any content with a max of the width of the
   // browser window.
   let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
-  let width = Math.min(maxWidth, this.document.documentElement.scrollWidth);
 
-  // Add scrollbar width to content size in case a scrollbar is needed.
-  width += this.scrollbarWidth;
+  // Adjust max width according to OS.
+  // We'd like to put this in CSS but we can't:
+  //   body { width: calc(min(-5px, max-content)); }
+  //   #_panel { max-width: -5px; }
+  switch(OS) {
+    case "Linux":
+      maxWidth -= 5;
+      break;
+    case "Darwin":
+      maxWidth -= 25;
+      break;
+    case "WINNT":
+      maxWidth -= 5;
+      break;
+  }
+
+  this.document.body.style.width = "-moz-max-content";
+  let style = this._frame.contentWindow.getComputedStyle(this.document.body);
+  let frameWidth = parseInt(style.width, 10);
+  let width = Math.min(maxWidth, frameWidth);
+  this.document.body.style.width = width + "px";
 
   // Set the width of the iframe.
   this._frame.style.minWidth = width + "px";
+  this._panel.style.maxWidth = maxWidth + "px";
 
   // browserAdjustment is used to correct the panel height according to the
   // browsers borders etc.
   const browserAdjustment = 15;
 
   // Set max panel height to match any content with a max of the height of the
   // browser window.
   let maxHeight =
@@ -901,38 +898,48 @@ OutputPanel.prototype._outputChanged = f
 {
   if (aEvent.output.hidden) {
     return;
   }
 
   this.remove();
 
   this.displayedOutput = aEvent.output;
-  this.update();
+  this.displayedOutput.onClose.add(this.remove, this);
 
-  this.displayedOutput.onChange.add(this.update, this);
-  this.displayedOutput.onClose.add(this.remove, this);
+  if (this.displayedOutput.completed) {
+    this._update();
+  }
+  else {
+    this.displayedOutput.promise.then(this._update, this._update)
+                                .then(null, console.error);
+  }
 };
 
 /**
  * Called when displayed Output says it's changed or from outputChanged, which
  * happens when there is a new displayed Output.
  */
-OutputPanel.prototype.update = function OP_update()
+OutputPanel.prototype._update = function OP_update()
 {
+  // destroy has been called, bail out
+  if (this._div == null) {
+    return;
+  }
+
   // Empty this._div
   while (this._div.hasChildNodes()) {
     this._div.removeChild(this._div.firstChild);
   }
 
   if (this.displayedOutput.data != null) {
     let requisition = this._devtoolbar.display.requisition;
     let nodePromise = converters.convert(this.displayedOutput.data,
                                          this.displayedOutput.type, 'dom',
-                                         requisition.context);
+                                         requisition.conversionContext);
     nodePromise.then(function(node) {
       while (this._div.hasChildNodes()) {
         this._div.removeChild(this._div.firstChild);
       }
 
       var links = node.ownerDocument.querySelectorAll('*[href]');
       for (var i = 0; i < links.length; i++) {
         links[i].setAttribute('target', '_blank');
@@ -953,17 +960,16 @@ OutputPanel.prototype.remove = function 
     this.canHide = true;
   }
 
   if (this._panel && this._panel.hidePopup) {
     this._panel.hidePopup();
   }
 
   if (this.displayedOutput) {
-    this.displayedOutput.onChange.remove(this.update, this);
     this.displayedOutput.onClose.remove(this.remove, this);
     delete this.displayedOutput;
   }
 };
 
 /**
  * Detach listeners from the currently displayed Output.
  */
--- a/browser/devtools/shared/test/Makefile.in
+++ b/browser/devtools/shared/test/Makefile.in
@@ -9,17 +9,16 @@ srcdir    = @srcdir@
 VPATH     = @srcdir@
 relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 DISABLED_XPCSHELL_TESTS = unit
 
 MOCHITEST_BROWSER_FILES = \
-  browser_browser_basic.js \
   browser_require_basic.js \
   browser_templater_basic.js \
   browser_toolbar_basic.js \
   browser_toolbar_tooltip.js \
   browser_toolbar_webconsole_errors_count.js \
   browser_layoutHelpers.js \
   browser_eventemitter_basic.js \
   head.js \
deleted file mode 100644
--- a/browser/devtools/shared/test/browser_browser_basic.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Tests exports from Browser.jsm
-
-const TEST_URI = "data:text/html;charset=utf-8,<p id=id>Text</p>";
-
-let imported = {};
-Components.utils.import("resource:///modules/devtools/Browser.jsm", imported);
-
-registerCleanupFunction(function tearDown() {
-  imported = undefined;
-});
-
-function test() {
-  addTab(TEST_URI, function(browser, tab, document) {
-    runTest(browser, tab, document);
-  });
-}
-
-function runTest(browser, tab, document) {
-  var p = document.getElementById("id");
-
-  ok(p instanceof imported.Node, "Node correctly defined");
-  ok(p instanceof imported.HTMLElement, "HTMLElement correctly defined");
-
-  finish();
-}
--- a/browser/devtools/shared/test/browser_templater_basic.js
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -5,17 +5,17 @@
 
 /*
  * These tests run both in Mozilla/Mochitest and plain browsers (as does
  * domtemplate)
  * We should endevour to keep the source in sync.
  */
 
 var Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
-var template = Cu.import("resource:///modules/devtools/Templater.jsm", {}).template;
+var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
 
 const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html";
 
 function test() {
   addTab(TEST_URI, function() {
     info("Starting DOM Templater Tests");
     runTest(0);
   });
--- a/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
+++ b/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
@@ -30,16 +30,18 @@ this.EXPORTED_SYMBOLS = ["BreadcrumbsWid
  *   myMethod: function() {},
  *   ...
  * });
  *
  * @param nsIDOMNode aNode
  *        The element associated with the widget.
  */
 this.BreadcrumbsWidget = function BreadcrumbsWidget(aNode) {
+  this.document = aNode.ownerDocument;
+  this.window = this.document.defaultView;
   this._parent = aNode;
 
   // Create an internal arrowscrollbox container.
   this._list = this.document.createElement("arrowscrollbox");
   this._list.className = "breadcrumbs-widget-container";
   this._list.setAttribute("flex", "1");
   this._list.setAttribute("orient", "horizontal");
   this._list.setAttribute("clicktoscroll", "true")
@@ -54,19 +56,16 @@ this.BreadcrumbsWidget = function Breadc
 
   // Delegate some of the associated node's methods to satisfy the interface
   // required by MenuContainer instances.
   ViewHelpers.delegateWidgetAttributeMethods(this, aNode);
   ViewHelpers.delegateWidgetEventMethods(this, aNode);
 };
 
 BreadcrumbsWidget.prototype = {
-  get document() this._parent.ownerDocument,
-  get window() this.document.defaultView,
-
   /**
    * Inserts an item in this container at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this item.
    * @param string | nsIDOMNode aContents
    *        The string or node displayed in the container.
    * @return nsIDOMNode
@@ -103,23 +102,22 @@ BreadcrumbsWidget.prototype = {
       this._selectedItem = null;
     }
   },
 
   /**
    * Removes all of the child nodes from this container.
    */
   removeAllItems: function BCW_removeAllItems() {
-    let parent = this._parent;
     let list = this._list;
-    let firstChild;
 
-    while (firstChild = list.firstChild) {
-      list.removeChild(firstChild);
+    while (list.hasChildNodes()) {
+      list.firstChild.remove();
     }
+
     this._selectedItem = null;
   },
 
   /**
    * Gets the currently selected child node in this container.
    * @return nsIDOMNode
    */
   get selectedItem() this._selectedItem,
@@ -141,21 +139,21 @@ BreadcrumbsWidget.prototype = {
       } else {
         node.removeAttribute("checked");
       }
     }
 
     // Repeated calls to ensureElementIsVisible would interfere with each other
     // and may sometimes result in incorrect scroll positions.
     this.window.clearTimeout(this._ensureVisibleTimeout);
-    this._ensureVisibleTimeout = this.window.setTimeout(function() {
+    this._ensureVisibleTimeout = this.window.setTimeout(() => {
       if (this._selectedItem) {
         this._list.ensureElementIsVisible(this._selectedItem);
       }
-    }.bind(this), ENSURE_SELECTION_VISIBLE_DELAY);
+    }, ENSURE_SELECTION_VISIBLE_DELAY);
   },
 
   /**
    * The underflow and overflow listener for the arrowscrollbox container.
    */
   _onUnderflow: function BCW__onUnderflow({target}) {
     if (target != this._list) {
       return;
@@ -172,43 +170,44 @@ BreadcrumbsWidget.prototype = {
     if (target != this._list) {
       return;
     }
     target._scrollButtonUp.collapsed = false;
     target._scrollButtonDown.collapsed = false;
     target.setAttribute("overflows", "");
   },
 
+  window: null,
+  document: null,
   _parent: null,
   _list: null,
   _selectedItem: null,
   _ensureVisibleTimeout: null
 };
 
 /**
  * A Breadcrumb constructor for the BreadcrumbsWidget.
  *
  * @param BreadcrumbsWidget aWidget
  *        The widget to contain this breadcrumb.
  * @param string | nsIDOMNode aContents
  *        The string or node displayed in the container.
  */
 function Breadcrumb(aWidget, aContents) {
+  this.document = aWidget.document;
+  this.window = aWidget.window;
   this.ownerView = aWidget;
 
   this._target = this.document.createElement("hbox");
   this._target.className = "breadcrumbs-widget-item";
   this._target.setAttribute("align", "center");
   this.contents = aContents;
 }
 
 Breadcrumb.prototype = {
-  get document() this.ownerView.document,
-  get window() this.document.defaultView,
-
   /**
    * Sets the contents displayed in this item's view.
    *
    * @param string | nsIDOMNode aContents
    *        The string or node displayed in the container.
    */
   set contents(aContents) {
     // If this item's view contents are a string, then create a label to hold
@@ -223,11 +222,13 @@ Breadcrumb.prototype = {
     if (this._target.hasChildNodes()) {
       this._target.replaceChild(aContents, this._target.firstChild);
       return;
     }
     // These are the first contents ever displayed.
     this._target.appendChild(aContents);
   },
 
+  window: null,
+  document: null,
   ownerView: null,
   _target: null
 };
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -32,16 +32,18 @@ this.EXPORTED_SYMBOLS = ["SideMenuWidget
  * });
  *
  * @param nsIDOMNode aNode
  *        The element associated with the widget.
  * @param boolean aShowArrows
  *        Specifies if items in this container should display horizontal arrows.
  */
 this.SideMenuWidget = function SideMenuWidget(aNode, aShowArrows = true) {
+  this.document = aNode.ownerDocument;
+  this.window = this.document.defaultView;
   this._parent = aNode;
   this._showArrows = aShowArrows;
 
   // Create an internal scrollbox container.
   this._list = this.document.createElement("scrollbox");
   this._list.className = "side-menu-widget-container";
   this._list.setAttribute("flex", "1");
   this._list.setAttribute("orient", "vertical");
@@ -55,29 +57,34 @@ this.SideMenuWidget = function SideMenuW
   this._orderedMenuElementsArray = [];
 
   // Delegate some of the associated node's methods to satisfy the interface
   // required by MenuContainer instances.
   ViewHelpers.delegateWidgetEventMethods(this, aNode);
 };
 
 SideMenuWidget.prototype = {
-  get document() this._parent.ownerDocument,
-  get window() this.document.defaultView,
+  /**
+   * Specifies if groups in this container should be sorted alphabetically.
+   */
+  sortedGroups: true,
 
   /**
    * Specifies if this container should try to keep the selected item visible.
    * (For example, when new items are added the selection is brought into view).
    */
   maintainSelectionVisible: true,
 
   /**
-   * Specifies if groups in this container should be sorted alphabetically.
+   * Specifies that the container viewport should be "stuck" to the
+   * bottom. That is, the container is automatically scrolled down to
+   * keep appended items visible, but only when the scroll position is
+   * already at the bottom.
    */
-  sortedGroups: true,
+  autoscrollWithAppendedItems: false,
 
   /**
    * Inserts an item in this container at the specified index, optionally
    * grouping by name.
    *
    * @param number aIndex
    *        The position in the container intended for this item.
    * @param string | nsIDOMNode aContents
@@ -85,22 +92,36 @@ SideMenuWidget.prototype = {
    * @param string aTooltip [optional]
    *        A tooltip attribute for the displayed item.
    * @param string aGroup [optional]
    *        The group to place the displayed item into.
    * @return nsIDOMNode
    *         The element associated with the displayed item.
    */
   insertItemAt: function SMW_insertItemAt(aIndex, aContents, aTooltip = "", aGroup = "") {
+    // Invalidate any notices set on this widget.
+    this.removeAttribute("notice");
+
+    let maintainScrollAtBottom =
+      this.autoscrollWithAppendedItems &&
+      (aIndex < 0 || aIndex >= this._orderedMenuElementsArray.length) &&
+      (this._list.scrollTop + this._list.clientHeight >= this._list.scrollHeight);
+
+    let group = this._getGroupForName(aGroup);
+    let item = this._getItemForGroup(group, aContents, aTooltip);
+    let element = item.insertSelfAt(aIndex);
+
     if (this.maintainSelectionVisible) {
-      this.ensureSelectionIsVisible(true, true); // Don't worry, it's delayed.
+      this.ensureSelectionIsVisible({ withGroup: true, delayed: true });
+    }
+    if (maintainScrollAtBottom) {
+      this._list.scrollTop = this._list.scrollHeight;
     }
 
-    let group = this._getGroupForName(aGroup);
-    return group.insertItemAt(aIndex, aContents, aTooltip, this._showArrows);
+    return element;
   },
 
   /**
    * Returns the child node in this container situated at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this item.
    * @return nsIDOMNode
@@ -112,38 +133,43 @@ SideMenuWidget.prototype = {
 
   /**
    * Removes the specified child node from this container.
    *
    * @param nsIDOMNode aChild
    *        The element associated with the displayed item.
    */
   removeChild: function SMW_removeChild(aChild) {
-    // Remove the item itself, not the contents.
-    let item = aChild.parentNode;
-    item.parentNode.removeChild(item);
+    if (aChild.className == "side-menu-widget-item-contents") {
+      // Remove the item itself, not the contents.
+      aChild.parentNode.remove();
+    } else {
+      // Groups with no title don't have any special internal structure.
+      aChild.remove();
+    }
+
     this._orderedMenuElementsArray.splice(
       this._orderedMenuElementsArray.indexOf(aChild), 1);
 
     if (this._selectedItem == aChild) {
       this._selectedItem = null;
     }
   },
 
   /**
    * Removes all of the child nodes from this container.
    */
   removeAllItems: function SMW_removeAllItems() {
     let parent = this._parent;
     let list = this._list;
-    let firstChild;
 
-    while (firstChild = list.firstChild) {
-      list.removeChild(firstChild);
+    while (list.hasChildNodes()) {
+      list.firstChild.remove();
     }
+
     this._selectedItem = null;
 
     this._groupsByName.clear();
     this._orderedGroupElementsArray.length = 0;
     this._orderedMenuElementsArray.length = 0;
   },
 
   /**
@@ -152,72 +178,75 @@ SideMenuWidget.prototype = {
    */
   get selectedItem() this._selectedItem,
 
   /**
    * Sets the currently selected child node in this container.
    * @param nsIDOMNode aChild
    */
   set selectedItem(aChild) {
-    let menuElementsArray = this._orderedMenuElementsArray;
+    let menuArray = this._orderedMenuElementsArray;
 
     if (!aChild) {
       this._selectedItem = null;
     }
-    for (let node of menuElementsArray) {
+    for (let node of menuArray) {
       if (node == aChild) {
         node.classList.add("selected");
         node.parentNode.classList.add("selected");
         this._selectedItem = node;
       } else {
         node.classList.remove("selected");
         node.parentNode.classList.remove("selected");
       }
     }
+
     // Repeated calls to ensureElementIsVisible would interfere with each other
     // and may sometimes result in incorrect scroll positions.
-    this.ensureSelectionIsVisible(false, true);
+    this.ensureSelectionIsVisible({ delayed: true });
   },
 
   /**
    * Ensures the selected element is visible.
    * @see SideMenuWidget.prototype.ensureElementIsVisible.
    */
-  ensureSelectionIsVisible:
-  function SMW_ensureSelectionIsVisible(aGroupFlag, aDelayedFlag) {
-    this.ensureElementIsVisible(this.selectedItem, aGroupFlag, aDelayedFlag);
+  ensureSelectionIsVisible: function SMW_ensureSelectionIsVisible(aFlags) {
+    this.ensureElementIsVisible(this.selectedItem, aFlags);
   },
 
   /**
    * Ensures the specified element is visible.
    *
    * @param nsIDOMNode aElement
    *        The element to make visible.
-   * @param boolean aGroupFlag
-   *        True if the group header should also be made visible, if possible.
-   * @param boolean aDelayedFlag
-   *        True to wait a few cycles before ensuring the selection is visible.
+   * @param object aFlags [optional]
+   *        An object containing some of the following flags:
+   *        - withGroup: true if the group header should also be made visible, if possible
+   *        - delayed: wait a few cycles before ensuring the selection is visible
    */
-  ensureElementIsVisible:
-  function SMW_ensureElementIsVisible(aElement, aGroupFlag, aDelayedFlag) {
+  ensureElementIsVisible: function SMW_ensureElementIsVisible(aElement, aFlags = {}) {
     if (!aElement) {
       return;
     }
-    if (aDelayedFlag) {
+
+    if (aFlags.delayed) {
+      delete aFlags.delayed;
       this.window.clearTimeout(this._ensureVisibleTimeout);
-      this._ensureVisibleTimeout = this.window.setTimeout(function() {
-        this.ensureElementIsVisible(aElement, aGroupFlag, false);
-      }.bind(this), ENSURE_SELECTION_VISIBLE_DELAY);
+      this._ensureVisibleTimeout = this.window.setTimeout(() => {
+        this.ensureElementIsVisible(aElement, aFlags);
+      }, ENSURE_SELECTION_VISIBLE_DELAY);
       return;
     }
-    if (aGroupFlag) {
+
+    if (aFlags.withGroup) {
       let groupList = aElement.parentNode;
       let groupContainer = groupList.parentNode;
       groupContainer.scrollIntoView(true); // Align with the top.
     }
+
     this._boxObject.ensureElementIsVisible(aElement);
   },
 
   /**
    * Shows all the groups, even the ones with no visible children.
    */
   showEmptyGroups: function SMW_showEmptyGroups() {
     for (let group of this._orderedGroupElementsArray) {
@@ -344,16 +373,34 @@ SideMenuWidget.prototype = {
     }
 
     let group = new SideMenuGroup(this, aName);
     this._groupsByName.set(aName, group);
     group.insertSelfAt(this.sortedGroups ? group.findExpectedIndexForSelf() : -1);
     return group;
   },
 
+  /**
+   * Gets a menu item to be displayed inside a group.
+   * @see SideMenuWidget.prototype._getGroupForName
+   *
+   * @param SideMenuGroup aGroup
+   *        The group to contain the menu item.
+   * @param string | nsIDOMNode aContents
+   *        The string or node displayed in the container.
+   * @param string aTooltip [optional]
+   *        A tooltip attribute for the displayed item.
+   */
+  _getItemForGroup: function SMW__getItemForGroup(aGroup, aContents, aTooltip) {
+    return new SideMenuItem(aGroup, aContents, aTooltip, this._showArrows);
+  },
+
+  window: null,
+  document: null,
+  _showArrows: false,
   _parent: null,
   _list: null,
   _boxObject: null,
   _selectedItem: null,
   _groupsByName: null,
   _orderedGroupElementsArray: null,
   _orderedMenuElementsArray: null,
   _ensureVisibleTimeout: null,
@@ -367,90 +414,64 @@ SideMenuWidget.prototype = {
  * Represents a group which should contain SideMenuItems.
  *
  * @param SideMenuWidget aWidget
  *        The widget to contain this menu item.
  * @param string aName
  *        The string displayed in the container.
  */
 function SideMenuGroup(aWidget, aName) {
+  this.document = aWidget.document;
+  this.window = aWidget.window;
   this.ownerView = aWidget;
   this.identifier = aName;
 
-  let document = this.document;
-  let title = this._title = document.createElement("hbox");
-  title.className = "side-menu-widget-group-title";
+  // Create an internal title and list container.
+  if (aName) {
+    let target = this._target = this.document.createElement("vbox");
+    target.className = "side-menu-widget-group";
+    target.setAttribute("name", aName);
+    target.setAttribute("tooltiptext", aName);
 
-  let name = this._name = document.createElement("label");
-  name.className = "plain name";
-  name.setAttribute("value", aName);
-  name.setAttribute("crop", "end");
-  name.setAttribute("flex", "1");
+    let list = this._list = this.document.createElement("vbox");
+    list.className = "side-menu-widget-group-list";
+
+    let title = this._title = this.document.createElement("hbox");
+    title.className = "side-menu-widget-group-title";
 
-  let list = this._list = document.createElement("vbox");
-  list.className = "side-menu-widget-group-list";
+    let name = this._name = this.document.createElement("label");
+    name.className = "plain name";
+    name.setAttribute("value", aName);
+    name.setAttribute("crop", "end");
+    name.setAttribute("flex", "1");
 
-  let target = this._target = document.createElement("vbox");
-  target.className = "side-menu-widget-group side-menu-widget-item-or-group";
-  target.setAttribute("name", aName);
-  target.setAttribute("tooltiptext", aName);
-
-  title.appendChild(name);
-  target.appendChild(title);
-  target.appendChild(list);
+    title.appendChild(name);
+    target.appendChild(title);
+    target.appendChild(list);
+  }
+  // Skip a few redundant nodes when no title is shown.
+  else {
+    let target = this._target = this._list = this.document.createElement("vbox");
+    target.className = "side-menu-widget-group side-menu-widget-group-list";
+  }
 }
 
 SideMenuGroup.prototype = {
-  get document() this.ownerView.document,
-  get window() this.document.defaultView,
-  get _groupElementsArray() this.ownerView._orderedGroupElementsArray,
-  get _menuElementsArray() this.ownerView._orderedMenuElementsArray,
-
-  /**
-   * Inserts an item in this group at the specified index.
-   *
-   * @param number aIndex
-   *        The position in the container intended for this item.
-   * @param string | nsIDOMNode aContents
-   *        The string or node displayed in the container.
-   * @param string aTooltip [optional]
-   *        A tooltip attribute for the displayed item.
-   * @param boolean aArrowFlag
-   *        True if a horizontal arrow should be shown.
-   * @return nsIDOMNode
-   *         The element associated with the displayed item.
-   */
-  insertItemAt: function SMG_insertItemAt(aIndex, aContents, aTooltip, aArrowFlag) {
-    let list = this._list;
-    let menuArray = this._menuElementsArray;
-    let item = new SideMenuItem(this, aContents, aTooltip, aArrowFlag);
-
-    // Invalidate any notices set on the owner widget.
-    this.ownerView.removeAttribute("notice");
-
-    if (aIndex >= 0) {
-      list.insertBefore(item._container, list.childNodes[aIndex]);
-      menuArray.splice(aIndex, 0, item._target);
-    } else {
-      list.appendChild(item._container);
-      menuArray.push(item._target);
-    }
-
-    return item._target;
-  },
+  get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
+  get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
 
   /**
    * Inserts this group in the parent container at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this group.
    */
   insertSelfAt: function SMG_insertSelfAt(aIndex) {
     let ownerList = this.ownerView._list;
-    let groupsArray = this._groupElementsArray;
+    let groupsArray = this._orderedGroupElementsArray;
 
     if (aIndex >= 0) {
       ownerList.insertBefore(this._target, groupsArray[aIndex]);
       groupsArray.splice(aIndex, 0, this._target);
     } else {
       ownerList.appendChild(this._target);
       groupsArray.push(this._target);
     }
@@ -459,28 +480,30 @@ SideMenuGroup.prototype = {
   /**
    * Finds the expected index of this group based on its name.
    *
    * @return number
    *         The expected index.
    */
   findExpectedIndexForSelf: function SMG_findExpectedIndexForSelf() {
     let identifier = this.identifier;
-    let groupsArray = this._groupElementsArray;
+    let groupsArray = this._orderedGroupElementsArray;
 
     for (let group of groupsArray) {
       let name = group.getAttribute("name");
       if (name > identifier && // Insertion sort at its best :)
          !name.contains(identifier)) { // Least significat group should be last.
         return groupsArray.indexOf(group);
       }
     }
     return -1;
   },
 
+  window: null,
+  document: null,
   ownerView: null,
   identifier: "",
   _target: null,
   _title: null,
   _name: null,
   _list: null
 };
 
@@ -492,50 +515,71 @@ SideMenuGroup.prototype = {
  * @param string aTooltip [optional]
  *        A tooltip attribute for the displayed item.
  * @param string | nsIDOMNode aContents
  *        The string or node displayed in the container.
  * @param boolean aArrowFlag
  *        True if a horizontal arrow should be shown.
  */
 function SideMenuItem(aGroup, aContents, aTooltip, aArrowFlag) {
+  this.document = aGroup.document;
+  this.window = aGroup.window;
   this.ownerView = aGroup;
 
-  let document = this.document;
-
   // Show a horizontal arrow towards the content.
   if (aArrowFlag) {
-    let target = this._target = document.createElement("vbox");
+    let container = this._container = this.document.createElement("hbox");
+    container.className = "side-menu-widget-item";
+    container.setAttribute("tooltiptext", aTooltip);
+
+    let target = this._target = this.document.createElement("vbox");
     target.className = "side-menu-widget-item-contents";
 
-    let arrow = this._arrow = document.createElement("hbox");
+    let arrow = this._arrow = this.document.createElement("hbox");
     arrow.className = "side-menu-widget-item-arrow";
 
-    let container = this._container = document.createElement("hbox");
-    container.className = "side-menu-widget-item side-menu-widget-item-or-group";
-    container.setAttribute("tooltiptext", aTooltip);
     container.appendChild(target);
     container.appendChild(arrow);
   }
   // Skip a few redundant nodes when no horizontal arrow is shown.
   else {
-    let target = this._target = this._container = document.createElement("hbox");
-    target.className =
-      "side-menu-widget-item " +
-      "side-menu-widget-item-or-group " +
-      "side-menu-widget-item-contents";
+    let target = this._target = this._container = this.document.createElement("hbox");
+    target.className = "side-menu-widget-item side-menu-widget-item-contents";
   }
 
   this._target.setAttribute("flex", "1");
   this.contents = aContents;
 }
 
 SideMenuItem.prototype = {
-  get document() this.ownerView.document,
-  get window() this.document.defaultView,
+  get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
+  get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+
+  /**
+   * Inserts this item in the parent group at the specified index.
+   *
+   * @param number aIndex
+   *        The position in the container intended for this item.
+   * @return nsIDOMNode
+   *         The element associated with the displayed item.
+   */
+  insertSelfAt: function SMI_insertSelfAt(aIndex) {
+    let ownerList = this.ownerView._list;
+    let menuArray = this._orderedMenuElementsArray;
+
+    if (aIndex >= 0) {
+      ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]);
+      menuArray.splice(aIndex, 0, this._target);
+    } else {
+      ownerList.appendChild(this._container);
+      menuArray.push(this._target);
+    }
+
+    return this._target;
+  },
 
   /**
    * Sets the contents displayed in this item's view.
    *
    * @param string | nsIDOMNode aContents
    *        The string or node displayed in the container.
    */
   set contents(aContents) {
@@ -554,13 +598,15 @@ SideMenuItem.prototype = {
     if (this._target.hasChildNodes()) {
       this._target.replaceChild(aContents, this._target.firstChild);
       return;
     }
     // These are the first contents ever displayed.
     this._target.appendChild(aContents);
   },
 
+  window: null,
+  document: null,
   ownerView: null,
   _target: null,
   _container: null,
   _arrow: null
 };
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -123,20 +123,19 @@ VariablesView.prototype = {
     }
     // Check if this empty operation may be executed lazily.
     if (this.lazyEmpty && aTimeout > 0) {
       this._emptySoon(aTimeout);
       return;
     }
 
     let list = this._list;
-    let firstChild;
-
-    while (firstChild = list.firstChild) {
-      list.removeChild(firstChild);
+
+    while (list.hasChildNodes()) {
+      list.firstChild.remove();
     }
 
     this._store.length = 0;
     this._itemsByElement.clear();
 
     this._appendEmptyNotice();
     this._toggleSearchVisibility(false);
   },
@@ -158,32 +157,32 @@ VariablesView.prototype = {
    */
   _emptySoon: function VV__emptySoon(aTimeout) {
     let prevList = this._list;
     let currList = this._list = this.document.createElement("scrollbox");
 
     this._store.length = 0;
     this._itemsByElement.clear();
 
-    this._emptyTimeout = this.window.setTimeout(function() {
+    this._emptyTimeout = this.window.setTimeout(() => {
       this._emptyTimeout = null;
 
       prevList.removeEventListener("keypress", this._onViewKeyPress, false);
       currList.addEventListener("keypress", this._onViewKeyPress, false);
       currList.setAttribute("orient", "vertical");
 
       this._parent.removeChild(prevList);
       this._parent.appendChild(currList);
       this._boxObject = currList.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
 
       if (!this._store.length) {
         this._appendEmptyNotice();
         this._toggleSearchVisibility(false);
       }
-    }.bind(this), aTimeout);
+    }, aTimeout);
   },
 
   /**
    * The amount of time (in milliseconds) it takes to empty this view lazily.
    */
   lazyEmptyDelay: LAZY_EMPTY_DELAY,
 
   /**
@@ -194,16 +193,22 @@ VariablesView.prototype = {
 
   /**
    * Specifies if nodes in this view may be added lazily.
    * @see Scope.prototype._lazyAppend
    */
   lazyAppend: true,
 
   /**
+   * Specifies if nodes in this view may be expanded lazily.
+   * @see Scope.prototype.expand
+   */
+  lazyExpand: true,
+
+  /**
    * Function called each time a variable or property's value is changed via
    * user interaction. If null, then value changes are disabled.
    *
    * This property is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   eval: null,
 
@@ -398,17 +403,17 @@ VariablesView.prototype = {
    * Disables variable and property searching in this view.
    * Use the "searchEnabled" setter to disable searching.
    */
   _disableSearch: function VV__disableSearch() {
     // If searching was already disabled, no need to re-disable it again.
     if (!this._searchboxContainer) {
       return;
     }
-    this._searchboxContainer.parentNode.removeChild(this._searchboxContainer);
+    this._searchboxContainer.remove();
     this._searchboxNode.removeEventListener("input", this._onSearchboxInput, false);
     this._searchboxNode.removeEventListener("keypress", this._onSearchboxKeyPress, false);
 
     this._searchboxContainer = null;
     this._searchboxNode = null;
   },
 
   /**
@@ -637,25 +642,27 @@ VariablesView.prototype = {
     let focused = this.document.commandDispatcher.focusedElement;
     return this.getItemForNode(focused);
   },
 
   /**
    * Focuses the next scope, variable or property in this view.
    * @see VariablesView.prototype._focusChange
    */
-  focusNextItem: function VV_focusNextItem(aMaintainViewFocusedFlag)
-    this._focusChange("advanceFocus", aMaintainViewFocusedFlag),
+  focusNextItem: function VV_focusNextItem(aMaintainViewFocusedFlag) {
+    this._focusChange("advanceFocus", aMaintainViewFocusedFlag)
+  },
 
   /**
    * Focuses the previous scope, variable or property in this view.
    * @see VariablesView.prototype._focusChange
    */
-  focusPrevItem: function VV_focusPrevItem(aMaintainViewFocusedFlag)
-    this._focusChange("rewindFocus", aMaintainViewFocusedFlag),
+  focusPrevItem: function VV_focusPrevItem(aMaintainViewFocusedFlag) {
+    this._focusChange("rewindFocus", aMaintainViewFocusedFlag)
+  },
 
   /**
    * Focuses the next or previous scope, variable or property in this view.
    *
    * @param string aDirection
    *        Either "advanceFocus" or "rewindFocus".
    * @param boolean aMaintainViewFocusedFlag
    *        True too keep this view focused if the element is out of bounds.
@@ -756,17 +763,16 @@ VariablesView.prototype = {
         }
         return;
 
       case e.DOM_VK_RIGHT:
         // Nothing to do here if this item never expands.
         if (!item._isArrowVisible) {
           return;
         }
-
         // Expand scopes, variables and properties before advancing focus.
         if (!item._isExpanded) {
           item.expand();
         } else {
           this.focusNextItem(true);
         }
         return;
 
@@ -1046,17 +1052,18 @@ VariablesView.getterOrSetterEvalMacro = 
  * Function invoked when a getter or setter is deleted.
  *
  * @param Property aItem
  *        The current getter or setter property.
  */
 VariablesView.getterOrSetterDeleteCallback = function(aItem) {
   aItem._disable();
 
-  // Make sure the right getter/setter to value override macro is applied to the target object.
+  // Make sure the right getter/setter to value override macro is applied
+  // to the target object.
   aItem.ownerView.eval(aItem.evaluationMacro(aItem, ""));
 
   return true; // Don't hide the element.
 };
 
 /**
  * A Scope is an object holding Variable instances.
  * Iterable via "for (let [name, variable] in instance) { }".
@@ -1232,17 +1239,17 @@ Scope.prototype = {
     if (this._isExpanded || this._locked) {
       return;
     }
     // If there's a large number of enumerable or non-enumerable items
     // contained in this scope, painting them may take several seconds,
     // even if they were already displayed before. In this case, show a throbber
     // to suggest that this scope is expanding.
     if (!this._isExpanding &&
-         this._variablesView.lazyAppend &&
+         this._variablesView.lazyExpand &&
          this._store.size > LAZY_APPEND_BATCH) {
       this._isExpanding = true;
 
       // Start spinning a throbber in this scope's title and allow a few
       // milliseconds for it to be painted.
       this._startThrobber();
       this.window.setTimeout(this.expand.bind(this), LAZY_EXPAND_DELAY);
       return;
@@ -1966,16 +1973,23 @@ Scope.prototype = {
  * @param object aDescriptor
  *        The variable's descriptor.
  */
 function Variable(aScope, aName, aDescriptor) {
   this._displayTooltip = this._displayTooltip.bind(this);
   this._activateNameInput = this._activateNameInput.bind(this);
   this._activateValueInput = this._activateValueInput.bind(this);
 
+  // Treat safe getter descriptors as descriptors with a value.
+  if ("getterValue" in aDescriptor) {
+    aDescriptor.value = aDescriptor.getterValue;
+    delete aDescriptor.get;
+    delete aDescriptor.set;
+  }
+
   Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
   this.setGrip(aDescriptor.value);
   this._symbolicName = aName;
   this._absoluteName = aScope.name + "[\"" + aName + "\"]";
 }
 
 ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
   /**
@@ -1990,16 +2004,18 @@ ViewHelpers.create({ constructor: Variab
    *        e.g. - { value: 42 }
    *             - { value: true }
    *             - { value: "nasu" }
    *             - { value: { type: "undefined" } }
    *             - { value: { type: "null" } }
    *             - { value: { type: "object", class: "Object" } }
    *             - { get: { type: "object", class: "Function" },
    *                 set: { type: "undefined" } }
+   *             - { get: { type "object", class: "Function" },
+   *                 getterValue: "foo", getterPrototypeLevel: 2 }
    * @param boolean aRelaxed
    *        True if name duplicates should be allowed.
    * @return Property
    *         The newly created Property instance, null if it already exists.
    */
   addProperty: function V_addProperty(aName = "", aDescriptor = {}, aRelaxed = false) {
     if (this._store.has(aName) && !aRelaxed) {
       return null;
@@ -2369,24 +2385,27 @@ ViewHelpers.create({ constructor: Variab
       let document = this.document;
 
       let tooltip = document.createElement("tooltip");
       tooltip.id = "tooltip-" + this._idString;
 
       let configurableLabel = document.createElement("label");
       let enumerableLabel = document.createElement("label");
       let writableLabel = document.createElement("label");
+      let safeGetterLabel = document.createElement("label");
       configurableLabel.setAttribute("value", "configurable");
       enumerableLabel.setAttribute("value", "enumerable");
       writableLabel.setAttribute("value", "writable");
+      safeGetterLabel.setAttribute("value", "native-getter");
 
       tooltip.setAttribute("orient", "horizontal");
       tooltip.appendChild(configurableLabel);
       tooltip.appendChild(enumerableLabel);
       tooltip.appendChild(writableLabel);
+      tooltip.appendChild(safeGetterLabel);
 
       this._target.appendChild(tooltip);
       this._target.setAttribute("tooltip", tooltip.id);
     }
     if (this.ownerView.eval && !this._isUndefined && (this.getter || this.setter)) {
       this._editNode.setAttribute("tooltiptext", this.ownerView.editButtonTooltip);
     }
     if (this.ownerView.eval) {
@@ -2415,16 +2434,19 @@ ViewHelpers.create({ constructor: Variab
       this._target.setAttribute("non-configurable", "");
     }
     if (!descriptor.null && !descriptor.enumerable) {
       this._target.setAttribute("non-enumerable", "");
     }
     if (!descriptor.null && !descriptor.writable && !this.ownerView.getter && !this.ownerView.setter) {
       this._target.setAttribute("non-writable", "");
     }
+    if (descriptor && "getterValue" in descriptor) {
+      this._target.setAttribute("safe-getter", "");
+    }
     if (name == "this") {
       this._target.setAttribute("self", "");
     }
     else if (name == "<exception>") {
       this._target.setAttribute("exception", "");
     }
     else if (name == "__proto__") {
       this._target.setAttribute("proto", "");
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -300,53 +300,49 @@ ViewHelpers.Prefs.prototype = {
 
 /**
  * A generic MenuItem is used to describe elements present in a MenuContainer.
  * The label, value and description properties are necessarily strings.
  * Iterable via "for (let childItem in parentItem) { }".
  *
  * @param any aAttachment
  *        Some attached primitive/object.
- * @param string aLabel
- *        The label displayed in the container.
- * @param string aValue
- *        The actual internal value of the item.
- * @param string aDescription [optional]
- *        An optional description of the item.
+ * @param nsIDOMNode | nsIDOMDocumentFragment | array aContents [optional]
+ *        A prebuilt node, or an array containing the following properties:
+ *        - aLabel: the label displayed in the container
+ *        - aValue: the actual internal value of the item
+ *        - aDescription: an optional description of the item
  */
-this.MenuItem = function MenuItem(aAttachment, aLabel, aValue, aDescription) {
+this.MenuItem = function MenuItem(aAttachment, aContents = []) {
   this.attachment = aAttachment;
-  this._label = aLabel + "";
-  this._value = aValue + "";
-  this._description = (aDescription || "") + "";
+
+  // Allow the insertion of prebuilt nodes.
+  if (aContents instanceof Ci.nsIDOMNode ||
+      aContents instanceof Ci.nsIDOMDocumentFragment) {
+    this._prebuiltTarget = aContents;
+  }
+  // Delegate the item view creation to a container widget.
+  else {
+    let [aLabel, aValue, aDescription] = aContents;
+    this._label = aLabel + "";
+    this._value = aValue + "";
+    this._description = (aDescription || "") + "";
+  }
 };
 
 MenuItem.prototype = {
-  /**
-   * Gets the label set for this item.
-   * @return string
-   */
   get label() this._label,
-
-  /**
-   * Gets the value set for this item.
-   * @return string
-   */
   get value() this._value,
-
-  /**
-   * Gets the description set for this item.
-   * @return string
-   */
   get description() this._description,
+  get target() this._target,
 
   /**
    * Immediately appends a child item to this menu item.
    *
-   * @param nsIDOMNode
+   * @param nsIDOMNode aElement
    *        An nsIDOMNode representing the child element to append.
    * @param object aOptions [optional]
    *        Additional options or flags supported by this operation:
    *          - attachment: some attached primitive/object for the item
    *          - attributes: a batch of attributes set to the displayed element
    *          - finalize: function called when the child node is removed
    * @return MenuItem
    *         The item associated with the displayed element.
@@ -358,65 +354,65 @@ MenuItem.prototype = {
     if (aOptions.attributes) {
       this.setAttributes(aOptions.attributes);
     }
     if (aOptions.finalize) {
       item.finalize = aOptions.finalize;
     }
 
     // Entangle the item with the newly inserted child node.
-    this._entangleItem(item, this.target.appendChild(aElement));
+    this._entangleItem(item, this._target.appendChild(aElement));
 
     // Return the item associated with the displayed element.
     return item;
   },
 
   /**
    * Immediately removes the specified child item from this menu item.
    *
    * @param MenuItem aItem
    *        The item associated with the element to remove.
    */
   remove: function MI_remove(aItem) {
     if (!aItem) {
       return;
     }
-    this.target.removeChild(aItem.target);
+    this._target.removeChild(aItem._target);
     this._untangleItem(aItem);
   },
 
   /**
    * Visually marks this menu item as selected.
    */
   markSelected: function MI_markSelected() {
-    if (!this.target) {
+    if (!this._target) {
       return;
     }
-    this.target.classList.add("selected");
+    this._target.classList.add("selected");
   },
 
   /**
    * Visually marks this menu item as deselected.
    */
   markDeselected: function MI_markDeselected() {
-    if (!this.target) {
+    if (!this._target) {
       return;
     }
-    this.target.classList.remove("selected");
+    this._target.classList.remove("selected");
   },
 
   /**
    * Batch sets attributes on an element.
    *
    * @param array aAttributes
    *        An array of [name, value] tuples representing the attributes.
    * @param nsIDOMNode aElement [optional]
    *        A custom element to set the attributes to.
    */
-  setAttributes: function MI_setAttributes(aAttributes, aElement = this.target) {
+  setAttributes: function MI_setAttributes(aAttributes, aElement = this._target) {
     for (let [name, value] of aAttributes) {
       aElement.setAttribute(name, value);
     }
   },
 
   /**
    * Entangles an item (model) with a displayed node element (view).
    *
@@ -426,35 +422,46 @@ MenuItem.prototype = {
    *        The element displaying the item.
    */
   _entangleItem: function MI__entangleItem(aItem, aElement) {
     if (!this._itemsByElement) {
       this._itemsByElement = new Map(); // This needs to be iterable.
     }
 
     this._itemsByElement.set(aElement, aItem);
-    aItem.target = aElement;
+    aItem._target = aElement;
   },
 
   /**
    * Untangles an item (model) from a displayed node element (view).
    *
    * @param MenuItem aItem
    *        The item describing the element.
    */
   _untangleItem: function MI__untangleItem(aItem) {
     if (aItem.finalize) {
       aItem.finalize(aItem);
     }
     for (let childItem in aItem) {
       aItem.remove(childItem);
     }
 
-    this._itemsByElement.delete(aItem.target);
-    aItem.target = null;
+    this._unlinkItem(aItem);
+    aItem._prebuiltTarget = null;
+    aItem._target = null;
+  },
+
+  /**
+   * Deletes an item from the its parent's storage maps.
+   *
+   * @param MenuItem aItem
+   *        The item to forget.
+   */
+  _unlinkItem: function MC__unlinkItem(aItem) {
+    this._itemsByElement.delete(aItem._target);
   },
 
   /**
    * Returns a string representing the object.
    * @return string
    */
   toString: function MI_toString() {
     if (this._label && this._value) {
@@ -464,17 +471,18 @@ MenuItem.prototype = {
       return this.attachment.toString();
     }
     return "(null)";
   },
 
   _label: "",
   _value: "",
   _description: "",
-  target: null,
+  _prebuiltTarget: null,
+  _target: null,
   finalize: null,
   attachment: null
 };
 
 /**
  * A generic MenuContainer is used for displaying MenuItem instances.
  * Iterable via "for (let item in menuContainer) { }".
  *
@@ -532,71 +540,65 @@ MenuContainer.prototype = {
    * sorted by their label. This can be overridden with the "index" flag,
    * specifying on which position should an item be appended.
    *
    * Furthermore, this container makes sure that all the items are unique
    * (two items with the same label or value are not allowed) and non-degenerate
    * (items with "undefined" or "null" labels/values). This can, as well, be
    * overridden via the "relaxed" flag.
    *
-   * @param nsIDOMNode | object aContents
-   *        An nsIDOMNode, or an array containing the following properties:
+   * @param nsIDOMNode | nsIDOMDocumentFragment array aContents
+   *        A prebuilt node, or an array containing the following properties:
    *          - label: the label displayed in the container
    *          - value: the actual internal value of the item
    *          - description: an optional description of the item
    * @param object aOptions [optional]
    *        Additional options or flags supported by this operation:
    *          - staged: true to stage the item to be appended later
    *          - index: specifies on which position should the item be appended
    *          - relaxed: true if this container should allow dupes & degenerates
    *          - attachment: some attached primitive/object for the item
    *          - attributes: a batch of attributes set to the displayed element
    *          - finalize: function called when the item is untangled (removed)
    * @return MenuItem
    *         The item associated with the displayed element if an unstaged push,
    *         undefined if the item was staged for a later commit.
    */
   push: function MC_push(aContents, aOptions = {}) {
-    if (aContents instanceof Ci.nsIDOMNode ||
-        aContents instanceof Ci.nsIDOMElement) {
-      // Allow the insertion of prebuilt nodes.
-      aOptions.node = aContents;
-      aContents = ["", "", ""];
-    }
-
-    let [label, value, description] = aContents;
-    let item = new MenuItem(aOptions.attachment, label, value, description);
+    let item = new MenuItem(aOptions.attachment, aContents);
 
     // Batch the item to be added later.
     if (aOptions.staged) {
+      // Commit operations will ignore any specified index.
+      delete aOptions.index;
       return void this._stagedItems.push({ item: item, options: aOptions });
     }
     // Find the target position in this container and insert the item there.
     if (!("index" in aOptions)) {
-      return this._insertItemAt(this._findExpectedIndex(label), item, aOptions);
+      return this._insertItemAt(this._findExpectedIndex(item), item, aOptions);
     }
     // Insert the item at the specified index. If negative or out of bounds,
     // the item will be simply appended.
     return this._insertItemAt(aOptions.index, item, aOptions);
   },
 
   /**
    * Flushes all the prepared items into this container.
+   * Any specified index on the items will be ignored. Everything is appended.
    *
    * @param object aOptions [optional]
    *        Additional options or flags supported by this operation:
    *          - sorted: true to sort all the items before adding them
    */
   commit: function MC_commit(aOptions = {}) {
     let stagedItems = this._stagedItems;
 
     // Sort the items before adding them to this container, if preferred.
     if (aOptions.sorted) {
-      stagedItems.sort(function(a, b) a.item._label.toLowerCase() >
-                                      b.item._label.toLowerCase());
+      stagedItems.sort((a, b) => this._sortPredicate(a.item, b.item));
     }
     // Append the prepared items to this container.
     for (let { item, options } of stagedItems) {
       this._insertItemAt(-1, item, options);
     }
     // Recreate the temporary items list for ulterior pushes.
     this._stagedItems.length = 0;
   },
@@ -625,17 +627,17 @@ MenuContainer.prototype = {
    *
    * @param MenuItem aItem
    *        The item associated with the element to remove.
    */
   remove: function MC_remove(aItem) {
     if (!aItem) {
       return;
     }
-    this._container.removeChild(aItem.target);
+    this._container.removeChild(aItem._target);
     this._untangleItem(aItem);
   },
 
   /**
    * Removes all items from this container.
    */
   empty: function MC_empty() {
     this._preferredValue = this.selectedValue;
@@ -679,18 +681,105 @@ MenuContainer.prototype = {
   /**
    * Toggles all the items in this container hidden or visible.
    *
    * @param boolean aVisibleFlag
    *        Specifies the intended visibility.
    */
   toggleContents: function MC_toggleContents(aVisibleFlag) {
     for (let [, item] of this._itemsByElement) {
-      item.target.hidden = !aVisibleFlag;
+      item._target.hidden = !aVisibleFlag;
+    }
+  },
+
+  /**
+   * Sorts all the items in this container based on a predicate.
+   *
+   * @param function aPredicate [optional]
+   *        Items are sorted according to the return value of the function, which
+   *        will become the new default sorting predicate in this container.
+   *        If unspecified, all items will be sorted by their label.
+   */
+  sortContents: function MC_sortContents(aPredicate = this._sortPredicate) {
+    let sortedItems = this.allItems.sort(this._sortPredicate = aPredicate);
+
+    for (let i = 0, len = sortedItems.length; i < len; i++) {
+      this.swapItems(this.getItemAtIndex(i), sortedItems[i]);
+    }
+  },
+
+  /**
+   * Visually swaps two items in this container.
+   *
+   * @param MenuItem aFirst
+   *        The first menu item to be swapped.
+   * @param MenuItem aSecond
+   *        The second menu item to be swapped.
+   */
+  swapItems: function MC_swapItems(aFirst, aSecond) {
+    if (aFirst == aSecond) { // We're just dandy, thank you.
+      return;
+    }
+    let { _prebuiltTarget: firstPrebuiltTarget, target: firstTarget } = aFirst;
+    let { _prebuiltTarget: secondPrebuiltTarget, target: secondTarget } = aSecond;
+
+    // If the two items were constructed with prebuilt nodes as DocumentFragments,
+    // then those DocumentFragments are now empty and need to be reassembled.
+    if (firstPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
+      for (let node of firstTarget.childNodes) {
+        firstPrebuiltTarget.appendChild(node.cloneNode(true));
+      }
     }
+    if (secondPrebuiltTarget instanceof Ci.nsIDOMDocumentFragment) {
+      for (let node of secondTarget.childNodes) {
+        secondPrebuiltTarget.appendChild(node.cloneNode(true));
+      }
+    }
+
+    // 1. Get the indices of the two items to swap.
+    let i = this._indexOfElement(firstTarget);
+    let j = this._indexOfElement(secondTarget);
+
+    // 2. Remeber the selection index, to reselect an item, if necessary.
+    let selectedTarget = this._container.selectedItem;
+    let selectedIndex = -1;
+    if (selectedTarget == firstTarget) {
+      selectedIndex = i;
+    } else if (selectedTarget == secondTarget) {
+      selectedIndex = j;
+    }
+
+    // 3. Silently nuke both items, nobody needs to know about this.
+    this._container.removeChild(firstTarget);
+    this._container.removeChild(secondTarget);
+    this._unlinkItem(aFirst);
+    this._unlinkItem(aSecond);
+
+    // 4. Add the items again, but reversing their indices.
+    this._insertItemAt.apply(this, i < j ? [i, aSecond] : [j, aFirst]);
+    this._insertItemAt.apply(this, i < j ? [j, aFirst] : [i, aSecond]);
+
+    // 5. Restore the previous selection, if necessary.
+    if (selectedIndex == i) {
+      this._container.selectedItem = aFirst._target;
+    } else if (selectedIndex == j) {
+      this._container.selectedItem = aSecond._target;
+    }
+  },
+
+  /**
+   * Visually swaps two items in this container at specific indices.
+   *
+   * @param number aFirst
+   *        The index of the first menu item to be swapped.
+   * @param number aSecond
+   *        The index of the second menu item to be swapped.
+   */
+  swapItemsAtIndices: function MC_swapItemsAtIndices(aFirst, aSecond) {
+    this.swapItems(this.getItemAtIndex(aFirst), this.getItemAtIndex(aSecond));
   },
 
   /**
    * Checks whether an item with the specified label is among the elements
    * shown in this container.
    *
    * @param string aLabel
    *        The item's label.
@@ -771,24 +860,25 @@ MenuContainer.prototype = {
   },
 
   /**
    * Selects the element with the entangled item in this container.
    * @param MenuItem aItem
    */
   set selectedItem(aItem) {
     // A falsy item is allowed to invalidate the current selection.
-    let targetNode = aItem ? aItem.target : null;
+    let targetElement = aItem ? aItem._target : null;
 
     // Prevent selecting the same item again, so return early.
-    if (this._container.selectedItem == targetNode) {
+    if (this._container.selectedItem == targetElement) {
       return;
     }
-    this._container.selectedItem = targetNode;
-    ViewHelpers.dispatchEvent(targetNode, "select", aItem);
+
+    this._container.selectedItem = targetElement;
+    ViewHelpers.dispatchEvent(targetElement, "select", aItem);
   },
 
   /**
    * Selects the element at the specified index in this container.
    * @param number aIndex
    */
   set selectedIndex(aIndex) {
     let targetElement = this._container.getItemAtIndex(aIndex);
@@ -872,17 +962,17 @@ MenuContainer.prototype = {
    * Finds the index of an item in the container.
    *
    * @param MenuItem aItem
    *        The item get the index for.
    * @return number
    *         The index of the matched item, or -1 if nothing is found.
    */
   indexOfItem: function MC_indexOfItem(aItem) {
-    return this._indexOfElement(aItem.target);
+    return this._indexOfElement(aItem._target);
   },
 
   /**
    * Finds the index of an element in the container.
    *
    * @param nsIDOMNode aElement
    *        The element get the index for.
    * @return number
@@ -926,17 +1016,30 @@ MenuContainer.prototype = {
 
   /**
    * Gets the total number of items in this container.
    * @return number
    */
   get itemCount() this._itemsByElement.size,
 
   /**
-   * Returns a list of all the visible (non-hidden) items in this container.
+   * Returns a list of all items in this container, in the displayed order.
+   * @return array
+   */
+  get allItems() {
+    let items = [];
+    for (let i = 0; i < this.itemCount; i++) {
+      items.push(this.getItemAtIndex(i));
+    }
+    return items;
+  },
+
+  /**
+   * Returns a list of all the visible (non-hidden) items in this container,
+   * in no particular order.
    * @return array
    */
   get visibleItems() {
     let items = [];
     for (let [element, item] of this._itemsByElement) {
       if (!element.hidden) {
         items.push(item);
       }
@@ -982,35 +1085,36 @@ MenuContainer.prototype = {
    * Checks if an item's label and value are eligible for this container.
    *
    * @param MenuItem aItem
    *        An object containing a label and a value property (at least).
    * @return boolean
    *         True if the element is eligible, false otherwise.
    */
   isEligible: function MC_isEligible(aItem) {
-    return this.isUnique(aItem) &&
+    return aItem._prebuiltTarget || (this.isUnique(aItem) &&
            aItem._label != "undefined" && aItem._label != "null" &&
-           aItem._value != "undefined" && aItem._value != "null";
+           aItem._value != "undefined" && aItem._value != "null");
   },
 
   /**
-   * Finds the expected item index in this container based on its label.
+   * Finds the expected item index in this container based on the default
+   * sort predicate.
    *
-   * @param string aLabel
-   *        The label used to identify the element.
+   * @param MenuItem aItem
+   *        The item to get the expected index for.
    * @return number
    *         The expected item index.
    */
-  _findExpectedIndex: function MC__findExpectedIndex(aLabel) {
+  _findExpectedIndex: function MC__findExpectedIndex(aItem) {
     let container = this._container;
     let itemCount = this.itemCount;
 
     for (let i = 0; i < itemCount; i++) {
-      if (this.getItemAtIndex(i)._label > aLabel) {
+      if (this._sortPredicate(this.getItemAtIndex(i), aItem) > 0) {
         return i;
       }
     }
     return itemCount;
   },
 
   /**
    * Immediately inserts an item in this container at the specified index.
@@ -1023,32 +1127,32 @@ MenuContainer.prototype = {
    *        Additional options or flags supported by this operation:
    *          - node: allows the insertion of prebuilt nodes instead of labels
    *          - relaxed: true if this container should allow dupes & degenerates
    *          - attributes: a batch of attributes set to the displayed element
    *          - finalize: function called when the item is untangled (removed)
    * @return MenuItem
    *         The item associated with the displayed element, null if rejected.
    */
-  _insertItemAt: function MC__insertItemAt(aIndex, aItem, aOptions) {
+  _insertItemAt: function MC__insertItemAt(aIndex, aItem, aOptions = {}) {
     // Relaxed nodes may be appended without verifying their eligibility.
     if (!aOptions.relaxed && !this.isEligible(aItem)) {
       return null;
     }
 
     // Entangle the item with the newly inserted node.
     this._entangleItem(aItem, this._container.insertItemAt(aIndex,
-      aOptions.node || aItem._label,
+      aItem._prebuiltTarget || aItem._label, // Allow the insertion of prebuilt nodes.
       aItem._value,
       aItem._description,
-      aOptions.attachment));
+      aItem.attachment));
 
     // Handle any additional options after entangling the item.
     if (aOptions.attributes) {
-      aItem.setAttributes(aOptions.attributes, aItem.target);
+      aItem.setAttributes(aOptions.attributes, aItem._target);
     }
     if (aOptions.finalize) {
       aItem.finalize = aOptions.finalize;
     }
 
     // Return the item associated with the displayed element.
     return aItem;
   },
@@ -1060,37 +1164,65 @@ MenuContainer.prototype = {
    *        The item describing the element.
    * @param nsIDOMNode aElement
    *        The element displaying the item.
    */
   _entangleItem: function MC__entangleItem(aItem, aElement) {
     this._itemsByLabel.set(aItem._label, aItem);
     this._itemsByValue.set(aItem._value, aItem);
     this._itemsByElement.set(aElement, aItem);
-    aItem.target = aElement;
+    aItem._target = aElement;
   },
 
   /**
    * Untangles an item (model) from a displayed node element (view).
    *
    * @param MenuItem aItem
    *        The item describing the element.
    */
   _untangleItem: function MC__untangleItem(aItem) {
     if (aItem.finalize) {
       aItem.finalize(aItem);
     }
     for (let childItem in aItem) {
       aItem.remove(childItem);
     }
 
+    this._unlinkItem(aItem);
+    aItem._prebuiltTarget = null;
+    aItem._target = null;
+  },
+
<