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 id24668
push userryanvm@gmail.com
push dateMon, 13 May 2013 15:59:04 +0000
treeherdermozilla-central@d7ce90899997 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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 eli