Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Fri, 20 Dec 2013 18:28:28 -0800
changeset 171791 4a1cc256b563d400a22337b3147113ef8c8eef98
parent 171769 33c392700cac8dc8276e34778cce698704a4d69f (current diff)
parent 171790 b1550c3edd5dcb6eaa39aa0f91f03c22efad2d1e (diff)
child 171868 90c67da3f8274c98ece5b90174a9151cdf614218
push id5166
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:47:54 +0000
treeherdermozilla-aurora@977eb2548b2d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
Merge fx-team to m-c
browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
mobile/android/base/strings.xml.in
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 878935 landed without a UUID change (and was since backed out)
+Bug 910189 requires a clobber due to Proguard inner class errors from bug 946083.
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -1690,17 +1690,20 @@ let CustomizableUIInternal = {
     } else {
       let autoAdd = true;
       try {
         autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd);
       } catch (e) {}
 
       // If the widget doesn't have an existing placement, and it hasn't been
       // seen before, then add it to its default area so it can be used.
-      if (autoAdd && !widget.currentArea && !gSeenWidgets.has(widget.id)) {
+      // If the widget is not removable, we *have* to add it to its default
+      // area here.
+      let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
+      if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
         this.beginBatchUpdate();
         try {
           gSeenWidgets.add(widget.id);
 
           if (widget.defaultArea) {
             if (this.isAreaLazy(widget.defaultArea)) {
               gFuturePlacements.get(widget.defaultArea).add(widget.id);
             } else {
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -43,9 +43,10 @@ skip-if = os == "mac"
 [browser_940013_registerToolbarNode_calls_registerArea.js]
 [browser_940946_removable_from_navbar_customizemode.js]
 [browser_941083_invalidate_wrapper_cache_createWidget.js]
 [browser_942581_unregisterArea_keeps_placements.js]
 [browser_943683_migration_test.js]
 [browser_944887_destroyWidget_should_destroy_in_palette.js]
 [browser_945739_showInPrivateBrowsing_customize_mode.js]
 [browser_947987_removable_default.js]
+[browser_948985_non_removable_defaultArea.js]
 [browser_panel_toggle.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js
@@ -0,0 +1,32 @@
+/* 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 kWidgetId = "test-destroy-non-removable-defaultArea";
+
+add_task(function() {
+  let spec = {id: kWidgetId, label: "Test non-removable defaultArea re-adding.",
+              removable: false, defaultArea: CustomizableUI.AREA_NAVBAR};
+  CustomizableUI.createWidget(spec);
+  let placement = CustomizableUI.getPlacementOfWidget(kWidgetId);
+  ok(placement, "Should have placed the widget.");
+  is(placement && placement.area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar");
+  CustomizableUI.destroyWidget(kWidgetId);
+  CustomizableUI.removeWidgetFromArea(kWidgetId);
+
+  CustomizableUI.createWidget(spec);
+  ok(placement, "Should have placed the widget.");
+  is(placement && placement.area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar");
+  CustomizableUI.destroyWidget(kWidgetId);
+  CustomizableUI.removeWidgetFromArea(kWidgetId);
+
+  const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
+  Services.prefs.setBoolPref(kPrefCustomizationAutoAdd, false);
+  CustomizableUI.createWidget(spec);
+  ok(placement, "Should have placed the widget.");
+  is(placement && placement.area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar");
+  CustomizableUI.destroyWidget(kWidgetId);
+  CustomizableUI.removeWidgetFromArea(kWidgetId);
+  Services.prefs.clearUserPref(kPrefCustomizationAutoAdd);
+});
+
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -578,16 +578,35 @@ nsBrowserContentHandler.prototype = {
             willRestoreSession = ss.isAutomaticRestoreEnabled();
 
             overridePage = Services.urlFormatter.formatURLPref("startup.homepage_override_url");
             if (prefb.prefHasUserValue("app.update.postupdate"))
               overridePage = getPostUpdateOverridePage(overridePage);
 
             overridePage = overridePage.replace("%OLD_VERSION%", old_mstone);
             break;
+
+          // Temporary case for Australis whatsnew
+          case OVERRIDE_NEW_BUILD_ID:
+            let locale = "en-US";
+            try {
+              locale = Services.prefs.getCharPref("general.useragent.locale");
+            } catch (e) {}
+
+            let showedAustralisWhatsNew = false;
+            try {
+              showedAustralisWhatsNew = Services.prefs.getBoolPref("browser.showedAustralisWhatsNew");
+            } catch(e) {}
+
+            // Show the Australis whatsnew page for en-US if we haven't yet shown it
+            if (!showedAustralisWhatsNew && locale == "en-US") {
+              Services.prefs.setBoolPref("browser.showedAustralisWhatsNew", true);
+              overridePage = "https://www.mozilla.org/en-US/firefox/29.0a1/whatsnew/";
+            }
+            break;
         }
       }
     } catch (ex) {}
 
     // formatURLPref might return "about:blank" if getting the pref fails
     if (overridePage == "about:blank")
       overridePage = "";
 
--- a/browser/components/sessionstore/src/TabState.jsm
+++ b/browser/components/sessionstore/src/TabState.jsm
@@ -177,27 +177,34 @@ let TabStateInternal = {
       let tabData = this._collectBaseTabData(tab);
 
       // Apply collected data.
       tabData.entries = history.entries;
       if ("index" in history) {
         tabData.index = history.index;
       }
 
-      // Copy data from the persistent cache.
-      this._copyFromPersistentCache(tab, tabData);
-
       // If we're still the latest async collection for the given tab and
       // the cache hasn't been filled by collect() in the meantime, let's
       // fill the cache with the data we received.
       if (this._pendingCollections.get(browser) == promise) {
         TabStateCache.set(tab, tabData);
         this._pendingCollections.delete(browser);
       }
 
+      // Copy data from the persistent cache. We need to create an explicit
+      // copy of the |tabData| object so that the properties injected by
+      // |_copyFromPersistentCache| don't end up in the non-persistent cache.
+      // The persistent cache does not store "null" values, so any values that
+      // have been cleared by the frame script would not be overriden by
+      // |_copyFromPersistentCache|. These two caches are only an interim
+      // solution and the non-persistent one will go away soon.
+      tabData = Utils.copy(tabData);
+      this._copyFromPersistentCache(tab, tabData);
+
       throw new Task.Result(tabData);
     }.bind(this));
 
     // Save the current promise as the latest asynchronous collection that is
     // running. This will be used to check whether the collected data is still
     // valid and will be used to fill the tab state cache.
     this._pendingCollections.set(browser, promise);
 
@@ -214,25 +221,44 @@ let TabStateInternal = {
    * tab has not been invalidated since the last call to
    * collectSync(aTab), the same object is returned.
    */
   collectSync: function (tab) {
     if (!tab) {
       throw new TypeError("Expecting a tab");
     }
     if (TabStateCache.has(tab)) {
-      return TabStateCache.get(tab);
+      // Copy data from the persistent cache. We need to create an explicit
+      // copy of the |tabData| object so that the properties injected by
+      // |_copyFromPersistentCache| don't end up in the non-persistent cache.
+      // The persistent cache does not store "null" values, so any values that
+      // have been cleared by the frame script would not be overriden by
+      // |_copyFromPersistentCache|. These two caches are only an interim
+      // solution and the non-persistent one will go away soon.
+      let tabData = Utils.copy(TabStateCache.get(tab));
+      this._copyFromPersistentCache(tab, tabData);
+      return tabData;
     }
 
     let tabData = this._collectSyncUncached(tab);
 
     if (this._tabCachingAllowed(tab)) {
       TabStateCache.set(tab, tabData);
     }
 
+    // Copy data from the persistent cache. We need to create an explicit
+    // copy of the |tabData| object so that the properties injected by
+    // |_copyFromPersistentCache| don't end up in the non-persistent cache.
+    // The persistent cache does not store "null" values, so any values that
+    // have been cleared by the frame script would not be overriden by
+    // |_copyFromPersistentCache|. These two caches are only an interim
+    // solution and the non-persistent one will go away soon.
+    tabData = Utils.copy(tabData);
+    this._copyFromPersistentCache(tab, tabData);
+
     // Prevent all running asynchronous collections from filling the cache.
     // Every asynchronous data collection started before a collectSync() call
     // can't expect to retrieve different data than the sync call. That's why
     // we just fill the cache with the data collected from the sync call and
     // discard any data collected asynchronously.
     this.dropPendingCollections(tab.linkedBrowser);
 
     return tabData;
@@ -257,17 +283,23 @@ let TabStateInternal = {
    * @param tab
    *        tabbrowser tab
    *
    * @returns {object} An object with the data for this tab. This data is never
    *                   cached, it will always be read from the tab and thus be
    *                   up-to-date.
    */
   clone: function (tab) {
-    return this._collectSyncUncached(tab, {includePrivateData: true});
+    let options = {includePrivateData: true};
+    let tabData = this._collectSyncUncached(tab, options);
+
+    // Copy data from the persistent cache.
+    this._copyFromPersistentCache(tab, tabData, options);
+
+    return tabData;
   },
 
   /**
    * Synchronously collect all session data for a tab. The
    * TabStateCache is not consulted, and the resulting data is not put
    * in the cache.
    */
   _collectSyncUncached: function (tab, options = {}) {
@@ -300,19 +332,16 @@ let TabStateInternal = {
       return tabData;
     }
 
     tabData.entries = history.entries;
     if ("index" in history) {
       tabData.index = history.index;
     }
 
-    // Copy data from the persistent cache.
-    this._copyFromPersistentCache(tab, tabData, options);
-
     return tabData;
   },
 
   /**
    * Copy tab data for the given |tab| from the persistent cache to |tabData|.
    *
    * @param tab (xul:tab)
    *        The tab belonging to the given |tabData| object.
--- a/browser/components/sessionstore/src/Utils.jsm
+++ b/browser/components/sessionstore/src/Utils.jsm
@@ -59,10 +59,21 @@ this.Utils = Object.freeze({
     if (map.has(otherKey)) {
       let otherValue = map.get(otherKey);
       map.set(key, otherValue);
       map.set(otherKey, value);
     } else {
       map.set(otherKey, value);
       map.delete(key);
     }
+  },
+
+  // Copies all properties of a given object to a new one and returns it.
+  copy: function (from) {
+    let to = {};
+
+    for (let key of Object.keys(from)) {
+      to[key] = from[key];
+    }
+
+    return to;
   }
 });
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -1923,17 +1923,20 @@ Breakpoints.prototype = {
       }
 
       // By default, new breakpoints are always enabled. Disabled breakpoints
       // are, in fact, removed from the server but preserved in the frontend,
       // so that they may not be forgotten across target navigations.
       let disabledPromise = this._disabled.get(identifier);
       if (disabledPromise) {
         disabledPromise.then(({ conditionalExpression: previousValue }) => {
-          aBreakpointClient.conditionalExpression = previousValue;
+          // Setting a falsy conditional expression is redundant.
+          if (previousValue) {
+            aBreakpointClient.conditionalExpression = previousValue;
+          }
         });
         this._disabled.delete(identifier);
       }
 
       // Preserve information about the breakpoint's line text, to display it
       // in the sources pane without requiring fetching the source (for example,
       // after the target navigated). Note that this will get out of sync
       // if the source text contents change.
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -95,16 +95,17 @@ support-files =
 [browser_dbg_clean-exit.js]
 [browser_dbg_closure-inspection.js]
 [browser_dbg_cmd-blackbox.js]
 [browser_dbg_cmd-break.js]
 [browser_dbg_cmd-dbg.js]
 [browser_dbg_conditional-breakpoints-01.js]
 [browser_dbg_conditional-breakpoints-02.js]
 [browser_dbg_conditional-breakpoints-03.js]
+[browser_dbg_conditional-breakpoints-04.js]
 [browser_dbg_controller-evaluate-01.js]
 [browser_dbg_controller-evaluate-02.js]
 [browser_dbg_debugger-statement.js]
 [browser_dbg_editor-contextmenu.js]
 [browser_dbg_editor-mode.js]
 [browser_dbg_event-listeners.js]
 [browser_dbg_file-reload.js]
 [browser_dbg_function-display-name.js]
--- a/browser/devtools/debugger/test/browser_dbg_closure-inspection.js
+++ b/browser/devtools/debugger/test/browser_dbg_closure-inspection.js
@@ -72,23 +72,23 @@ function test() {
           return;
         }
         window.clearInterval(intervalID);
 
         is(personNode.get("getName").target.querySelector(".name")
            .getAttribute("value"), "getName",
           "Should have the right property name for 'getName' in person.");
         is(personNode.get("getName").target.querySelector(".value")
-           .getAttribute("value"), "Function",
+           .getAttribute("value"), "_pfactory/<.getName()",
           "'getName' in person should have the right value.");
         is(personNode.get("getFoo").target.querySelector(".name")
            .getAttribute("value"), "getFoo",
           "Should have the right property name for 'getFoo' in person.");
         is(personNode.get("getFoo").target.querySelector(".value")
-           .getAttribute("value"), "Function",
+           .getAttribute("value"), "_pfactory/<.getFoo()",
           "'getFoo' in person should have the right value.");
 
         // Expand the function nodes. This causes their properties to be
         // retrieved and displayed.
         let getFooNode = personNode.get("getFoo");
         let getNameNode = personNode.get("getName");
         getFooNode.expand();
         getNameNode.expand();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints-04.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that conditional breakpoints with undefined expressions
+ * are stored as plain breakpoints when re-enabling them.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+  let gTab, gDebuggee, gPanel, gDebugger;
+  let gSources, gBreakpoints, gLocation;
+
+  initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPanel = aPanel;
+    gDebugger = gPanel.panelWin;
+    gSources = gDebugger.DebuggerView.Sources;
+    gBreakpoints = gDebugger.DebuggerController.Breakpoints;
+
+    gLocation = { url: gSources.selectedValue, line: 18 };
+
+    waitForSourceAndCaretAndScopes(gPanel, ".html", 17)
+      .then(addBreakpoint)
+      .then(setDummyConditional)
+      .then(() => {
+        let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED);
+        toggleBreakpoint();
+        return finished;
+      })
+      .then(() => {
+        let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
+        toggleBreakpoint();
+        return finished;
+      })
+      .then(testConditionalExpressionOnClient)
+      .then(() => {
+        let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
+        openConditionalPopup();
+        finished.then(() => ok(false, "The popup shouldn't have opened."));
+        return waitForTime(1000);
+      })
+      .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+      .then(null, aError => {
+        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+      });
+
+    gDebuggee.ermahgerd();
+  });
+
+  function addBreakpoint() {
+    return gPanel.addBreakpoint(gLocation);
+  }
+
+  function setDummyConditional(aClient) {
+    // This happens when a conditional expression input popup is shown
+    // but the user doesn't type anything into it.
+    aClient.conditionalExpression = "";
+  }
+
+  function toggleBreakpoint() {
+    EventUtils.sendMouseEvent({ type: "click" },
+      gDebugger.document.querySelector(".dbg-breakpoint-checkbox"),
+      gDebugger);
+  }
+
+  function openConditionalPopup() {
+    EventUtils.sendMouseEvent({ type: "click" },
+      gDebugger.document.querySelector(".dbg-breakpoint"),
+      gDebugger);
+  }
+
+  function testConditionalExpressionOnClient() {
+    return gBreakpoints._getAdded(gLocation).then(aClient => {
+      if ("conditionalExpression" in aClient) {
+        ok(false, "A conditional expression shouldn't have been set.");
+      } else {
+        ok(true, "The conditional expression wasn't set, as expected.");
+      }
+    });
+  }
+}
--- a/browser/devtools/debugger/test/browser_dbg_on-pause-highlight.js
+++ b/browser/devtools/debugger/test/browser_dbg_on-pause-highlight.js
@@ -25,23 +25,25 @@ function test() {
 }
 
 function testPause() {
   is(gDebugger.gThreadClient.paused, false,
     "Should be running after starting test.");
 
   gDebugger.gThreadClient.addOneTimeListener("paused", () => {
     gToolbox.selectTool("webconsole").then(() => {
-      ok(gToolboxTab.classList.contains("highlighted"),
+      ok(gToolboxTab.hasAttribute("highlighted") &&
+         gToolboxTab.getAttribute("highlighted") == "true",
         "The highlighted class is present");
       ok(!gToolboxTab.hasAttribute("selected") ||
           gToolboxTab.getAttribute("selected") != "true",
         "The tab is not selected");
     }).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
-      ok(gToolboxTab.classList.contains("highlighted"),
+      ok(gToolboxTab.hasAttribute("highlighted") &&
+         gToolboxTab.getAttribute("highlighted") == "true",
         "The highlighted class is present");
       ok(gToolboxTab.hasAttribute("selected") &&
          gToolboxTab.getAttribute("selected") == "true",
         "...and the tab is selected, so the glow will not be present.");
     }).then(testResume);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
--- a/browser/devtools/debugger/test/browser_dbg_on-pause-raise.js
+++ b/browser/devtools/debugger/test/browser_dbg_on-pause-raise.js
@@ -73,39 +73,42 @@ function testPause() {
     if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
       is(gFocusedWindow, gToolbox._host._window,
          "Toolbox window is the top level window on pause.");
     } else {
       is(gBrowser.selectedTab, gTab,
         "Debugger's tab got selected.");
     }
     gToolbox.selectTool("webconsole").then(() => {
-      ok(gToolboxTab.classList.contains("highlighted"),
+      ok(gToolboxTab.hasAttribute("highlighted") &&
+         gToolboxTab.getAttribute("highlighted") == "true",
         "The highlighted class is present");
       ok(!gToolboxTab.hasAttribute("selected") ||
           gToolboxTab.getAttribute("selected") != "true",
         "The tab is not selected");
     }).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
-      ok(gToolboxTab.classList.contains("highlighted"),
+      ok(gToolboxTab.hasAttribute("highlighted") &&
+         gToolboxTab.getAttribute("highlighted") == "true",
         "The highlighted class is present");
       ok(gToolboxTab.hasAttribute("selected") &&
          gToolboxTab.getAttribute("selected") == "true",
         "...and the tab is selected, so the glow will not be present.");
     }).then(testResume);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gDebugger.document.getElementById("resume"),
     gDebugger);
 }
 
 function testResume() {
   gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
     gToolbox.selectTool("webconsole").then(() => {
-      ok(!gToolboxTab.classList.contains("highlighted"),
+      ok(!gToolboxTab.hasAttribute("highlighted") ||
+          gToolboxTab.getAttribute("highlighted") != "true",
         "The highlighted class is not present now after the resume");
       ok(!gToolboxTab.hasAttribute("selected") ||
           gToolboxTab.getAttribute("selected") != "true",
         "The tab is not selected");
     }).then(maybeEndTest);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
--- a/browser/devtools/debugger/test/browser_dbg_pause-exceptions-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_pause-exceptions-01.js
@@ -53,17 +53,17 @@ function testPauseOnExceptionsDisabled()
 
     is(gFrames.itemCount, 1,
       "Should have one frame.");
     is(gVariables._store.length, 3,
       "Should have three scopes.");
 
     is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
       "Should have the right property name for 'this'.");
-    is(innerNodes[0].querySelector(".value").getAttribute("value"), "HTMLButtonElement",
+    is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
       "Should have the right property value for 'this'.");
 
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
       isnot(gDebugger.gThreadClient.state, "paused",
         "Should not be paused after resuming.");
       ok(isCaretPos(gPanel, 26),
         "Should be idle on the debugger statement.");
 
@@ -119,17 +119,17 @@ function testPauseOnExceptionsEnabled() 
 
       is(gFrames.itemCount, 1,
         "Should have one frame.");
       is(gVariables._store.length, 3,
         "Should have three scopes.");
 
       is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
         "Should have the right property name for 'this'.");
-      is(innerNodes[0].querySelector(".value").getAttribute("value"), "HTMLButtonElement",
+      is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
         "Should have the right property value for 'this'.");
 
       let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
         isnot(gDebugger.gThreadClient.state, "paused",
           "Should not be paused after resuming.");
         ok(isCaretPos(gPanel, 26),
           "Should be idle on the debugger statement.");
 
--- a/browser/devtools/debugger/test/browser_dbg_pause-exceptions-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_pause-exceptions-02.js
@@ -74,17 +74,17 @@ function testPauseOnExceptionsAfterReloa
 
       is(gFrames.itemCount, 1,
         "Should have one frame.");
       is(gVariables._store.length, 3,
         "Should have three scopes.");
 
       is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
         "Should have the right property name for 'this'.");
-      is(innerNodes[0].querySelector(".value").getAttribute("value"), "HTMLButtonElement",
+      is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
         "Should have the right property value for 'this'.");
 
       let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
         isnot(gDebugger.gThreadClient.state, "paused",
           "Should not be paused after resuming.");
         ok(isCaretPos(gPanel, 26),
           "Should be idle on the debugger statement.");
 
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-edit-value.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-edit-value.js
@@ -17,26 +17,26 @@ function test() {
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gVars = gDebugger.DebuggerView.Variables;
 
     waitForSourceAndCaretAndScopes(gPanel, ".html", 24)
       .then(() => initialChecks())
       .then(() => testModification("a", "1"))
       .then(() => testModification("{ a: 1 }", "Object"))
-      .then(() => testModification("[a]", "Array"))
+      .then(() => testModification("[a]", "Array[1]"))
       .then(() => testModification("b", "Object"))
       .then(() => testModification("b.a", "1"))
       .then(() => testModification("c.a", "1"))
       .then(() => testModification("Infinity", "Infinity"))
       .then(() => testModification("NaN", "NaN"))
-      .then(() => testModification("new Function", "Function"))
+      .then(() => testModification("new Function", "anonymous()"))
       .then(() => testModification("+0", "0"))
       .then(() => testModification("-0", "-0"))
-      .then(() => testModification("Object.keys({})", "Array"))
+      .then(() => testModification("Object.keys({})", "Array[0]"))
       .then(() => testModification("document.title", '"Debugger test page"'))
       .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
 
     EventUtils.sendMouseEvent({ type: "click" },
       gDebuggee.document.querySelector("button"),
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js
@@ -84,24 +84,26 @@ function testExpandVariables() {
   is(argsVar.expanded, false,
     "The argsVar should not be expanded at this point.");
   is(cVar.expanded, false,
     "The cVar should not be expanded at this point.");
 
   waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => {
     is(thisVar.get("window").target.querySelector(".name").getAttribute("value"), "window",
       "Should have the right property name for 'window'.");
-    is(thisVar.get("window").target.querySelector(".value").getAttribute("value"), "Window",
+    is(thisVar.get("window").target.querySelector(".value").getAttribute("value"),
+      "Window \u2192 doc_frame-parameters.html",
       "Should have the right property value for 'window'.");
     ok(thisVar.get("window").target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'window'.");
 
     is(thisVar.get("document").target.querySelector(".name").getAttribute("value"), "document",
       "Should have the right property name for 'document'.");
-    is(thisVar.get("document").target.querySelector(".value").getAttribute("value"), "HTMLDocument",
+    is(thisVar.get("document").target.querySelector(".value").getAttribute("value"),
+      "HTMLDocument \u2192 doc_frame-parameters.html",
       "Should have the right property value for 'document'.");
     ok(thisVar.get("document").target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'document'.");
 
     let argsProps = argsVar.target.querySelectorAll(".variables-view-property");
     is(argsProps.length, 8,
       "The 'arguments' variable should contain 5 enumerable and 3 non-enumerable properties");
 
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-02.js
@@ -51,17 +51,18 @@ function testScopeVariables() {
 
   is(localEnums.length, 12,
     "The local scope should contain all the created enumerable elements.");
   is(localNonEnums.length, 0,
     "The local scope should contain all the created non-enumerable elements.");
 
   is(localEnums[0].querySelector(".name").getAttribute("value"), "this",
     "Should have the right property name for 'this'.");
-  is(localEnums[0].querySelector(".value").getAttribute("value"), "Window",
+  is(localEnums[0].querySelector(".value").getAttribute("value"),
+    "Window \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'this'.");
   ok(localEnums[0].querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'this'.");
 
   is(localEnums[1].querySelector(".name").getAttribute("value"), "aArg",
     "Should have the right property name for 'aArg'.");
   is(localEnums[1].querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for 'aArg'.");
@@ -187,17 +188,18 @@ function testArgumentsProperties() {
       "Should have the right property name for '4'.");
     is(argsEnums[4].querySelector(".value").getAttribute("value"), "null",
       "Should have the right property name for '4'.");
     ok(argsEnums[4].querySelector(".value").className.contains("token-null"),
       "Should have the right token class for '4'.");
 
     is(argsNonEnums[0].querySelector(".name").getAttribute("value"), "callee",
      "Should have the right property name for 'callee'.");
-    is(argsNonEnums[0].querySelector(".value").getAttribute("value"), "Function",
+    is(argsNonEnums[0].querySelector(".value").getAttribute("value"),
+     "test(aArg,bArg,cArg,dArg,eArg,fArg)",
      "Should have the right property name for 'callee'.");
     ok(argsNonEnums[0].querySelector(".value").className.contains("token-other"),
      "Should have the right token class for 'callee'.");
 
     is(argsNonEnums[1].querySelector(".name").getAttribute("value"), "length",
       "Should have the right property name for 'length'.");
     is(argsNonEnums[1].querySelector(".value").getAttribute("value"), "5",
       "Should have the right property value for 'length'.");
@@ -513,24 +515,26 @@ function testGetterSetterObject() {
 
     is(propEnums.length, 0,
       "The propEnums should contain all the created enumerable elements.");
     is(propNonEnums.length, 2,
       "The propEnums should contain all the created non-enumerable elements.");
 
     is(propNonEnums[0].querySelector(".name").getAttribute("value"), "get",
       "Should have the right property name for 'get'.");
-    is(propNonEnums[0].querySelector(".value").getAttribute("value"), "Function",
+    is(propNonEnums[0].querySelector(".value").getAttribute("value"),
+      "test/myVar.prop()",
       "Should have the right property value for 'get'.");
     ok(propNonEnums[0].querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'get'.");
 
     is(propNonEnums[1].querySelector(".name").getAttribute("value"), "set",
       "Should have the right property name for 'set'.");
-    is(propNonEnums[1].querySelector(".value").getAttribute("value"), "Function",
+    is(propNonEnums[1].querySelector(".value").getAttribute("value"),
+      "test/myVar.prop(val)",
       "Should have the right property value for 'set'.");
     ok(propNonEnums[1].querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'set'.");
 
     deferred.resolve();
   });
 
   myVar.expand();
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-03.js
@@ -66,22 +66,24 @@ function testGlobalScope() {
 
   is(globalScope.get("SpecialPowers").target.querySelector(".name").getAttribute("value"), "SpecialPowers",
     "Should have the right property name for 'SpecialPowers'.");
   is(globalScope.get("SpecialPowers").target.querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for 'SpecialPowers'.");
 
   is(globalScope.get("window").target.querySelector(".name").getAttribute("value"), "window",
     "Should have the right property name for 'window'.");
-  is(globalScope.get("window").target.querySelector(".value").getAttribute("value"), "Window",
+  is(globalScope.get("window").target.querySelector(".value").getAttribute("value"),
+    "Window \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'window'.");
 
   is(globalScope.get("document").target.querySelector(".name").getAttribute("value"), "document",
     "Should have the right property name for 'document'.");
-  is(globalScope.get("document").target.querySelector(".value").getAttribute("value"), "HTMLDocument",
+  is(globalScope.get("document").target.querySelector(".value").getAttribute("value"),
+    "HTMLDocument \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'document'.");
 
   is(globalScope.get("undefined").target.querySelector(".name").getAttribute("value"), "undefined",
     "Should have the right property name for 'undefined'.");
   is(globalScope.get("undefined").target.querySelector(".value").getAttribute("value"), "undefined",
     "Should have the right property value for 'undefined'.");
 
   is(globalScope.get("undefined").target.querySelector(".enum").childNodes.length, 0,
@@ -118,22 +120,24 @@ function testWindowVariable() {
 
   is(windowVar.get("SpecialPowers").target.querySelector(".name").getAttribute("value"), "SpecialPowers",
     "Should have the right property name for 'SpecialPowers'.");
   is(windowVar.get("SpecialPowers").target.querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for 'SpecialPowers'.");
 
   is(windowVar.get("window").target.querySelector(".name").getAttribute("value"), "window",
     "Should have the right property name for 'window'.");
-  is(windowVar.get("window").target.querySelector(".value").getAttribute("value"), "Window",
+  is(windowVar.get("window").target.querySelector(".value").getAttribute("value"),
+    "Window \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'window'.");
 
   is(windowVar.get("document").target.querySelector(".name").getAttribute("value"), "document",
     "Should have the right property name for 'document'.");
-  is(windowVar.get("document").target.querySelector(".value").getAttribute("value"), "HTMLDocument",
+  is(windowVar.get("document").target.querySelector(".value").getAttribute("value"),
+    "HTMLDocument \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'document'.");
 
   is(windowVar.get("undefined").target.querySelector(".name").getAttribute("value"), "undefined",
     "Should have the right property name for 'undefined'.");
   is(windowVar.get("undefined").target.querySelector(".value").getAttribute("value"), "undefined",
     "Should have the right property value for 'undefined'.");
 
   is(windowVar.get("undefined").target.querySelector(".enum").childNodes.length, 0,
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-with.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-with.js
@@ -52,17 +52,18 @@ function testFirstWithScope() {
 
   is(withEnums.length, 3,
     "The first 'with' scope should contain all the created enumerable elements.");
   is(withNonEnums.length, 1,
     "The first 'with' scope should contain all the created non-enumerable elements.");
 
   is(withEnums[0].querySelector(".name").getAttribute("value"), "this",
     "Should have the right property name for 'this'.");
-  is(withEnums[0].querySelector(".value").getAttribute("value"), "Window",
+  is(withEnums[0].querySelector(".value").getAttribute("value"),
+    "Window \u2192 doc_with-frame.html",
     "Should have the right property value for 'this'.");
   ok(withEnums[0].querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'this'.");
 
   is(withEnums[1].querySelector(".name").getAttribute("value"), "one",
     "Should have the right property name for 'one'.");
   is(withEnums[1].querySelector(".value").getAttribute("value"), "1",
     "Should have the right property value for 'one'.");
@@ -126,17 +127,17 @@ function testSecondWithScope() {
     "Should have the right property name for 'PI'.");
   is(secondWithScope.get("PI").target.querySelector(".value").getAttribute("value"), "3.141592653589793",
     "Should have the right property value for 'PI'.");
   ok(secondWithScope.get("PI").target.querySelector(".value").className.contains("token-number"),
     "Should have the right token class for 'PI'.");
 
   is(secondWithScope.get("random").target.querySelector(".name").getAttribute("value"), "random",
     "Should have the right property name for 'random'.");
-  is(secondWithScope.get("random").target.querySelector(".value").getAttribute("value"), "Function",
+  is(secondWithScope.get("random").target.querySelector(".value").getAttribute("value"), "random()",
     "Should have the right property value for 'random'.");
   ok(secondWithScope.get("random").target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'random'.");
 
   is(secondWithScope.get("__proto__").target.querySelector(".name").getAttribute("value"), "__proto__",
     "Should have the right property name for '__proto__'.");
   is(secondWithScope.get("__proto__").target.querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for '__proto__'.");
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js
@@ -49,17 +49,17 @@ function initialChecks() {
     "Should have the right property name for 'buffer'.");
   is(bufferVar.target.querySelector(".value").getAttribute("value"), "ArrayBuffer",
     "Should have the right property value for 'buffer'.");
   ok(bufferVar.target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'buffer'.");
 
   is(arrayVar.target.querySelector(".name").getAttribute("value"), "largeArray",
     "Should have the right property name for 'largeArray'.");
-  is(arrayVar.target.querySelector(".value").getAttribute("value"), "Int8Array",
+  is(arrayVar.target.querySelector(".value").getAttribute("value"), "Int8Array[10000]",
     "Should have the right property value for 'largeArray'.");
   ok(arrayVar.target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'largeArray'.");
 
   is(objectVar.target.querySelector(".name").getAttribute("value"), "largeObject",
     "Should have the right property name for 'largeObject'.");
   is(objectVar.target.querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for 'largeObject'.");
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-webidl.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-webidl.js
@@ -57,31 +57,32 @@ function performTest() {
   let globalScope = gVariables.getScopeAtIndex(1);
 
   let buttonVar = globalScope.get("button");
   let buttonAsProtoVar = globalScope.get("buttonAsProto");
   let documentVar = globalScope.get("document");
 
   is(buttonVar.target.querySelector(".name").getAttribute("value"), "button",
     "Should have the right property name for 'button'.");
-  is(buttonVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElement",
+  is(buttonVar.target.querySelector(".value").getAttribute("value"), "<button>",
     "Should have the right property value for 'button'.");
   ok(buttonVar.target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'button'.");
 
   is(buttonAsProtoVar.target.querySelector(".name").getAttribute("value"), "buttonAsProto",
     "Should have the right property name for 'buttonAsProto'.");
   is(buttonAsProtoVar.target.querySelector(".value").getAttribute("value"), "Object",
     "Should have the right property value for 'buttonAsProto'.");
   ok(buttonAsProtoVar.target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'buttonAsProto'.");
 
   is(documentVar.target.querySelector(".name").getAttribute("value"), "document",
     "Should have the right property name for 'document'.");
-  is(documentVar.target.querySelector(".value").getAttribute("value"), "HTMLDocument",
+  is(documentVar.target.querySelector(".value").getAttribute("value"),
+    "HTMLDocument \u2192 doc_frame-parameters.html",
     "Should have the right property value for 'document'.");
   ok(documentVar.target.querySelector(".value").className.contains("token-other"),
     "Should have the right token class for 'document'.");
 
   is(buttonVar.expanded, false,
     "The buttonVar should not be expanded at this point.");
   is(buttonAsProtoVar.expanded, false,
     "The buttonAsProtoVar should not be expanded at this point.");
@@ -93,38 +94,38 @@ function performTest() {
       "Should have the right property name for 'type'.");
     is(buttonVar.get("type").target.querySelector(".value").getAttribute("value"), "\"submit\"",
       "Should have the right property value for 'type'.");
     ok(buttonVar.get("type").target.querySelector(".value").className.contains("token-string"),
       "Should have the right token class for 'type'.");
 
     is(buttonVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes",
       "Should have the right property name for 'childNodes'.");
-    is(buttonVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList",
+    is(buttonVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[1]",
       "Should have the right property value for 'childNodes'.");
     ok(buttonVar.get("childNodes").target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'childNodes'.");
 
     is(buttonVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick",
       "Should have the right property name for 'onclick'.");
-    is(buttonVar.get("onclick").target.querySelector(".value").getAttribute("value"), "Function",
+    is(buttonVar.get("onclick").target.querySelector(".value").getAttribute("value"), "onclick(event)",
       "Should have the right property value for 'onclick'.");
     ok(buttonVar.get("onclick").target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'onclick'.");
 
     is(documentVar.get("title").target.querySelector(".name").getAttribute("value"), "title",
       "Should have the right property name for 'title'.");
     is(documentVar.get("title").target.querySelector(".value").getAttribute("value"), "\"Debugger test page\"",
       "Should have the right property value for 'title'.");
     ok(documentVar.get("title").target.querySelector(".value").className.contains("token-string"),
       "Should have the right token class for 'title'.");
 
     is(documentVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes",
       "Should have the right property name for 'childNodes'.");
-    is(documentVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList",
+    is(documentVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[3]",
       "Should have the right property value for 'childNodes'.");
     ok(documentVar.get("childNodes").target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for 'childNodes'.");
 
     is(documentVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick",
       "Should have the right property name for 'onclick'.");
     is(documentVar.get("onclick").target.querySelector(".value").getAttribute("value"), "null",
       "Should have the right property value for 'onclick'.");
@@ -139,17 +140,17 @@ function performTest() {
       "Should have the right property name for '__proto__'.");
     is(buttonProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElementPrototype",
       "Should have the right property value for '__proto__'.");
     ok(buttonProtoVar.target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for '__proto__'.");
 
     is(buttonAsProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
       "Should have the right property name for '__proto__'.");
-    is(buttonAsProtoProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElement",
+    is(buttonAsProtoProtoVar.target.querySelector(".value").getAttribute("value"), "<button>",
       "Should have the right property value for '__proto__'.");
     ok(buttonAsProtoProtoVar.target.querySelector(".value").className.contains("token-other"),
       "Should have the right token class for '__proto__'.");
 
     is(documentProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__",
       "Should have the right property name for '__proto__'.");
     is(documentProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLDocumentPrototype",
       "Should have the right property value for '__proto__'.");
@@ -168,24 +169,24 @@ function performTest() {
         "Should have the right property name for 'type'.");
       is(buttonAsProtoProtoVar.get("type").target.querySelector(".value").getAttribute("value"), "\"submit\"",
         "Should have the right property value for 'type'.");
       ok(buttonAsProtoProtoVar.get("type").target.querySelector(".value").className.contains("token-string"),
         "Should have the right token class for 'type'.");
 
       is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes",
         "Should have the right property name for 'childNodes'.");
-      is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList",
+      is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[1]",
         "Should have the right property value for 'childNodes'.");
       ok(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").className.contains("token-other"),
         "Should have the right token class for 'childNodes'.");
 
       is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick",
         "Should have the right property name for 'onclick'.");
-      is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").getAttribute("value"), "Function",
+      is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").getAttribute("value"), "onclick(event)",
         "Should have the right property value for 'onclick'.");
       ok(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").className.contains("token-other"),
         "Should have the right token class for 'onclick'.");
 
       let buttonProtoProtoVar = buttonProtoVar.get("__proto__");
       let buttonAsProtoProtoProtoVar = buttonAsProtoProtoVar.get("__proto__");
       let documentProtoProtoVar = documentProtoVar.get("__proto__");
 
--- a/browser/devtools/framework/test/browser_toolbox_highlight.js
+++ b/browser/devtools/framework/test/browser_toolbox_highlight.js
@@ -60,25 +60,25 @@ function highlightTab(toolId) {
 
 function unhighlightTab(toolId) {
   info("Unhighlighting tool " + toolId + "'s tab.");
   toolbox.unhighlightTool(toolId);
 }
 
 function checkHighlighted(toolId) {
   let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
-  ok(tab.classList.contains("highlighted"), "The highlighted class is present");
+  ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
   ok(!tab.hasAttribute("selected") || tab.getAttribute("selected") != "true",
      "The tab is not selected");
 }
 
 function checkNoHighlightWhenSelected(toolId) {
   let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
-  ok(tab.classList.contains("highlighted"), "The highlighted class is present");
+  ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
   ok(tab.hasAttribute("selected") && tab.getAttribute("selected") == "true",
      "and the tab is selected, so the orange glow will not be present.");
 }
 
 function checkNoHighlight(toolId) {
   let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
-  ok(!tab.classList.contains("highlighted"),
-     "The highlighted class is not present");
+  ok(!tab.hasAttribute("highlighted"),
+     "The highlighted attribute is not present");
 }
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -791,28 +791,28 @@ Toolbox.prototype = {
   /**
    * Highlights the tool's tab if it is not the currently selected tool.
    *
    * @param {string} id
    *        The id of the tool to highlight
    */
   highlightTool: function(id) {
     let tab = this.doc.getElementById("toolbox-tab-" + id);
-    tab && tab.classList.add("highlighted");
+    tab && tab.setAttribute("highlighted", "true");
   },
 
   /**
    * De-highlights the tool's tab.
    *
    * @param {string} id
    *        The id of the tool to unhighlight
    */
   unhighlightTool: function(id) {
     let tab = this.doc.getElementById("toolbox-tab-" + id);
-    tab && tab.classList.remove("highlighted");
+    tab && tab.removeAttribute("highlighted");
   },
 
   /**
    * Raise the toolbox host.
    */
   raise: function() {
     this._host.raise();
   },
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -22,16 +22,19 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   "resource://gre/modules/devtools/Loader.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+  "resource://gre/modules/PluralForm.jsm");
+
 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
   "@mozilla.org/widget/clipboardhelper;1",
   "nsIClipboardHelper");
 
 Object.defineProperty(this, "WebConsoleUtils", {
   get: function() {
     return devtools.require("devtools/toolkit/webconsole/utils").Utils;
   },
@@ -2341,17 +2344,20 @@ Variable.prototype = Heritage.extend(Sco
       return;
     }
 
     let prevGrip = this._valueGrip;
     if (prevGrip) {
       this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
     }
     this._valueGrip = aGrip;
-    this._valueString = VariablesView.getString(aGrip, true);
+    this._valueString = VariablesView.getString(aGrip, {
+      concise: true,
+      noEllipsis: true,
+    });
     this._valueClassName = VariablesView.getClass(aGrip);
 
     this._valueLabel.classList.add(this._valueClassName);
     this._valueLabel.setAttribute("value", this._valueString);
     this._separatorLabel.hidden = false;
   },
 
   /**
@@ -3113,55 +3119,432 @@ VariablesView.getGrip = function(aValue)
   }
 };
 
 /**
  * Returns a custom formatted property string for a grip.
  *
  * @param any aGrip
  *        @see Variable.setGrip
- * @param boolean aConciseFlag
- *        Return a concisely formatted property string.
+ * @param object aOptions
+ *        Options:
+ *        - concise: boolean that tells you want a concisely formatted string.
+ *        - noStringQuotes: boolean that tells to not quote strings.
+ *        - noEllipsis: boolean that tells to not add an ellipsis after the
+ *        initial text of a longString.
  * @return string
  *         The formatted property string.
  */
-VariablesView.getString = function(aGrip, aConciseFlag) {
+VariablesView.getString = function(aGrip, aOptions = {}) {
   if (aGrip && typeof aGrip == "object") {
     switch (aGrip.type) {
       case "undefined":
       case "null":
       case "NaN":
       case "Infinity":
       case "-Infinity":
       case "-0":
         return aGrip.type;
-      case "longString":
-        return "\"" + aGrip.initial + "\"";
       default:
-        if (!aConciseFlag) {
-          return "[" + aGrip.type + " " + aGrip.class + "]";
+        let stringifier = VariablesView.stringifiers.byType[aGrip.type];
+        if (stringifier) {
+          let result = stringifier(aGrip, aOptions);
+          if (result != null) {
+            return result;
+          }
         }
-        return aGrip.class;
+
+        if (aGrip.displayString) {
+          return VariablesView.getString(aGrip.displayString, aOptions);
+        }
+
+        if (aGrip.type == "object" && aOptions.concise) {
+          return aGrip.class;
+        }
+
+        return "[" + aGrip.type + " " + aGrip.class + "]";
     }
   }
+
   switch (typeof aGrip) {
     case "string":
-      return "\"" + aGrip + "\"";
+      return VariablesView.stringifiers.byType.string(aGrip, aOptions);
     case "boolean":
       return aGrip ? "true" : "false";
     case "number":
       if (!aGrip && 1 / aGrip === -Infinity) {
         return "-0";
       }
     default:
       return aGrip + "";
   }
 };
 
 /**
+ * The VariablesView stringifiers are used by VariablesView.getString(). These
+ * are organized by object type, object class and by object actor preview kind.
+ * Some objects share identical ways for previews, for example Arrays, Sets and
+ * NodeLists.
+ *
+ * Any stringifier function must return a string. If null is returned, * then
+ * the default stringifier will be used. When invoked, the stringifier is
+ * given the same two arguments as those given to VariablesView.getString().
+ */
+VariablesView.stringifiers = {};
+
+VariablesView.stringifiers.byType = {
+  string: function(aGrip, {noStringQuotes}) {
+    if (noStringQuotes) {
+      return aGrip;
+    }
+    return uneval(aGrip);
+  },
+
+  longString: function({initial}, {noStringQuotes, noEllipsis}) {
+    let ellipsis = noEllipsis ? "" : Scope.ellipsis;
+    if (noStringQuotes) {
+      return initial + ellipsis;
+    }
+    let result = uneval(initial);
+    if (!ellipsis) {
+      return result;
+    }
+    return result.substr(0, result.length - 1) + ellipsis + '"';
+  },
+
+  object: function(aGrip, aOptions) {
+    let {preview} = aGrip;
+    let stringifier;
+    if (preview && preview.kind) {
+      stringifier = VariablesView.stringifiers.byObjectKind[preview.kind];
+    }
+    if (!stringifier && aGrip.class) {
+      stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class];
+    }
+    if (stringifier) {
+      return stringifier(aGrip, aOptions);
+    }
+    return null;
+  },
+}; // VariablesView.stringifiers.byType
+
+VariablesView.stringifiers.byObjectClass = {
+  Function: function(aGrip, {concise}) {
+    // TODO: Bug 948484 - support arrow functions and ES6 generators
+
+    let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
+    name = VariablesView.getString(name, { noStringQuotes: true });
+
+    // TODO: Bug 948489 - Support functions with destructured parameters and
+    // rest parameters
+    let params = aGrip.parameterNames || "";
+    if (!concise) {
+      return "function " + name + "(" + params + ")";
+    }
+    return (name || "function ") + "(" + params + ")";
+  },
+
+  RegExp: function({displayString}) {
+    return VariablesView.getString(displayString, { noStringQuotes: true });
+  },
+
+  Date: function({preview}) {
+    if (!preview || !("timestamp" in preview)) {
+      return null;
+    }
+
+    if (typeof preview.timestamp != "number") {
+      return new Date(preview.timestamp).toString(); // invalid date
+    }
+
+    return "Date " + new Date(preview.timestamp).toISOString();
+  },
+}; // VariablesView.stringifiers.byObjectClass
+
+VariablesView.stringifiers.byObjectKind = {
+  ArrayLike: function(aGrip, {concise}) {
+    let {preview} = aGrip;
+    if (concise) {
+      return aGrip.class + "[" + preview.length + "]";
+    }
+
+    if (!preview.items) {
+      return null;
+    }
+
+    let shown = 0, result = [], lastHole = null;
+    for (let item of preview.items) {
+      if (item === null) {
+        if (lastHole !== null) {
+          result[lastHole] += ",";
+        } else {
+          result.push("");
+        }
+        lastHole = result.length - 1;
+      } else {
+        lastHole = null;
+        result.push(VariablesView.getString(item, { concise: true }));
+      }
+      shown++;
+    }
+
+    if (shown < preview.length) {
+      let n = preview.length - shown;
+      result.push(VariablesView.stringifiers._getNMoreString(n));
+    } else if (lastHole !== null) {
+      // make sure we have the right number of commas...
+      result[lastHole] += ",";
+    }
+
+    let prefix = aGrip.class == "Array" ? "" : aGrip.class + " ";
+    return prefix + "[" + result.join(", ") + "]";
+  },
+
+  MapLike: function(aGrip, {concise}) {
+    let {preview} = aGrip;
+    if (concise || !preview.entries) {
+      let size = typeof preview.size == "number" ?
+                   "[" + preview.size + "]" : "";
+      return aGrip.class + size;
+    }
+
+    let entries = [];
+    for (let [key, value] of preview.entries) {
+      let keyString = VariablesView.getString(key, {
+        concise: true,
+        noStringQuotes: true,
+      });
+      let valueString = VariablesView.getString(value, { concise: true });
+      entries.push(keyString + ": " + valueString);
+    }
+
+    if (typeof preview.size == "number" && preview.size > entries.length) {
+      let n = preview.size - entries.length;
+      entries.push(VariablesView.stringifiers._getNMoreString(n));
+    }
+
+    return aGrip.class + " {" + entries.join(", ") + "}";
+  },
+
+  ObjectWithText: function(aGrip, {concise}) {
+    if (concise) {
+      return aGrip.class;
+    }
+
+    return aGrip.class + " " + VariablesView.getString(aGrip.preview.text);
+  },
+
+  ObjectWithURL: function(aGrip, {concise}) {
+    let result = aGrip.class;
+    let url = aGrip.preview.url;
+    if (!VariablesView.isFalsy({ value: url })) {
+      result += " \u2192 " + WebConsoleUtils.abbreviateSourceURL(url,
+                             { onlyCropQuery: !concise });
+    }
+    return result;
+  },
+
+  // Stringifier for any kind of object.
+  Object: function(aGrip, {concise}) {
+    if (concise) {
+      return aGrip.class;
+    }
+
+    let {preview} = aGrip;
+    let props = [];
+    for (let key of Object.keys(preview.ownProperties || {})) {
+      let value = preview.ownProperties[key];
+      let valueString = "";
+      if (value.get) {
+        valueString = "Getter";
+      } else if (value.set) {
+        valueString = "Setter";
+      } else {
+        valueString = VariablesView.getString(value.value, { concise: true });
+      }
+      props.push(key + ": " + valueString);
+    }
+
+    for (let key of Object.keys(preview.safeGetterValues || {})) {
+      let value = preview.safeGetterValues[key];
+      let valueString = VariablesView.getString(value.getterValue,
+                                                { concise: true });
+      props.push(key + ": " + valueString);
+    }
+
+    if (!props.length) {
+      return null;
+    }
+
+    if (preview.ownPropertiesLength) {
+      let previewLength = Object.keys(preview.ownProperties).length;
+      let diff = preview.ownPropertiesLength - previewLength;
+      if (diff > 0) {
+        props.push(VariablesView.stringifiers._getNMoreString(diff));
+      }
+    }
+
+    let prefix = aGrip.class != "Object" ? aGrip.class + " " : "";
+    return prefix + "{" + props.join(", ") + "}";
+  }, // Object
+
+  Error: function(aGrip, {concise}) {
+    let {preview} = aGrip;
+    let name = VariablesView.getString(preview.name, { noStringQuotes: true });
+    if (concise) {
+      return name || aGrip.class;
+    }
+
+    let msg = name + ": " +
+              VariablesView.getString(preview.message, { noStringQuotes: true });
+
+    if (!VariablesView.isFalsy({ value: preview.stack })) {
+      msg += "\n" + STR.GetStringFromName("variablesViewErrorStacktrace") +
+             "\n" + preview.stack;
+    }
+
+    return msg;
+  },
+
+  DOMException: function(aGrip, {concise}) {
+    let {preview} = aGrip;
+    if (concise) {
+      return preview.name || aGrip.class;
+    }
+
+    let msg = aGrip.class + " [" + preview.name + ": " +
+              VariablesView.getString(preview.message) + "\n" +
+              "code: " + preview.code + "\n" +
+              "nsresult: 0x" + (+preview.result).toString(16);
+
+    if (preview.filename) {
+      msg += "\nlocation: " + preview.filename;
+      if (preview.lineNumber) {
+        msg += ":" + preview.lineNumber;
+      }
+    }
+
+    return msg + "]";
+  },
+
+  DOMEvent: function(aGrip, {concise}) {
+    let {preview} = aGrip;
+    if (!preview.type) {
+      return null;
+    }
+
+    if (concise) {
+      return aGrip.class + " " + preview.type;
+    }
+
+    let result = preview.type;
+
+    if (preview.eventKind == "key" && preview.modifiers &&
+        preview.modifiers.length) {
+      result += " " + preview.modifiers.join("-");
+    }
+
+    let props = [];
+    if (preview.target) {
+      let target = VariablesView.getString(preview.target, { concise: true });
+      props.push("target: " + target);
+    }
+
+    for (let prop in preview.properties) {
+      let value = preview.properties[prop];
+      props.push(prop + ": " + VariablesView.getString(value, { concise: true }));
+    }
+
+    return result + " {" + props.join(", ") + "}";
+  }, // DOMEvent
+
+  DOMNode: function(aGrip, {concise}) {
+    let {preview} = aGrip;
+
+    switch (preview.nodeType) {
+      case Ci.nsIDOMNode.DOCUMENT_NODE: {
+        let location = WebConsoleUtils.abbreviateSourceURL(preview.location,
+                                                           { onlyCropQuery: !concise });
+        return aGrip.class + " \u2192 " + location;
+      }
+
+      case Ci.nsIDOMNode.ATTRIBUTE_NODE: {
+        let value = VariablesView.getString(preview.value, { noStringQuotes: true });
+        return preview.nodeName + '="' + escapeHTML(value) + '"';
+      }
+
+      case Ci.nsIDOMNode.TEXT_NODE:
+        return preview.nodeName + " " +
+               VariablesView.getString(preview.textContent);
+
+      case Ci.nsIDOMNode.COMMENT_NODE: {
+        let comment = VariablesView.getString(preview.textContent,
+                                              { noStringQuotes: true });
+        return "<!--" + comment + "-->";
+      }
+
+      case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: {
+        if (concise || !preview.childNodes) {
+          return aGrip.class + "[" + preview.childNodesLength + "]";
+        }
+        let nodes = [];
+        for (let node of preview.childNodes) {
+          nodes.push(VariablesView.getString(node));
+        }
+        if (nodes.length < preview.childNodesLength) {
+          let n = preview.childNodesLength - nodes.length;
+          nodes.push(VariablesView.stringifiers._getNMoreString(n));
+        }
+        return aGrip.class + " [" + nodes.join(", ") + "]";
+      }
+
+      case Ci.nsIDOMNode.ELEMENT_NODE: {
+        let attrs = preview.attributes;
+        if (!concise) {
+          let n = 0, result = "<" + preview.nodeName;
+          for (let name in attrs) {
+            let value = VariablesView.getString(attrs[name],
+                                                { noStringQuotes: true });
+            result += " " + name + '="' + escapeHTML(value) + '"';
+            n++;
+          }
+          if (preview.attributesLength > n) {
+            result += " " + Scope.ellipsis;
+          }
+          return result + ">";
+        }
+
+        let result = "<" + preview.nodeName;
+        if (attrs.id) {
+          result += "#" + attrs.id;
+        }
+        return result + ">";
+      }
+
+      default:
+        return null;
+    }
+  }, // DOMNode
+}; // VariablesView.stringifiers.byObjectKind
+
+
+/**
+ * Get the "N more…" formatted string, given an N. This is used for displaying
+ * how many elements are not displayed in an object preview (eg. an array).
+ *
+ * @private
+ * @param number aNumber
+ * @return string
+ */
+VariablesView.stringifiers._getNMoreString = function(aNumber) {
+  let str = STR.GetStringFromName("variablesViewMoreObjects");
+  return PluralForm.get(aNumber, str).replace("#1", aNumber);
+};
+
+/**
  * Returns a custom class style for a grip.
  *
  * @param any aGrip
  *        @see Variable.setGrip
  * @return string
  *         The custom class style.
  */
 VariablesView.getClass = function(aGrip) {
@@ -3204,16 +3587,32 @@ VariablesView.getClass = function(aGrip)
 let generateId = (function() {
   let count = 0;
   return function(aName = "") {
     return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count);
   };
 })();
 
 /**
+ * Escape some HTML special characters. We do not need full HTML serialization
+ * here, we just want to make strings safe to display in HTML attributes, for
+ * the stringifiers.
+ *
+ * @param string aString
+ * @return string
+ */
+function escapeHTML(aString) {
+  return aString.replace(/&/g, "&amp;")
+                .replace(/"/g, "&quot;")
+                .replace(/</g, "&lt;")
+                .replace(/>/g, "&gt;");
+}
+
+
+/**
  * An Editable encapsulates the UI of an edit box that overlays a label,
  * allowing the user to edit the value.
  *
  * @param Variable aVariable
  *        The Variable or Property to make editable.
  * @param object aOptions
  *        - onSave
  *          The callback to call with the value when editing is complete.
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -992,20 +992,22 @@ Messages.Extended.prototype = Heritage.e
       }
       if (piece.type == "longString") {
         let widget = new Widgets.LongString(this, piece).render();
         return widget.element;
       }
     }
 
     let result = this.document.createDocumentFragment();
-    if (!isPrimitive || (!this._quoteStrings && typeof piece == "string")) {
+    if (isPrimitive) {
+      result.textContent = VariablesView.getString(piece, {
+        noStringQuotes: !this._quoteStrings,
+      });
+    } else {
       result.textContent = piece;
-    } else {
-      result.textContent = VariablesView.getString(piece);
     }
 
     return result;
   },
 }); // Messages.Extended.prototype
 
 
 
@@ -1214,17 +1216,17 @@ Widgets.JSObject.prototype = Heritage.ex
 
   /**
    * The click event handler for objects shown inline.
    * @private
    */
   _onClick: function()
   {
     this.output.openVariablesView({
-      label: this.element.textContent,
+      label: VariablesView.getString(this.objectActor, { concise: true }),
       objectActor: this.objectActor,
       autofocus: true,
     });
   },
 }); // Widgets.JSObject.prototype
 
 /**
  * The long string widget.
@@ -1268,21 +1270,20 @@ Widgets.LongString.prototype = Heritage.
   /**
    * Render the long string in the widget element.
    * @private
    * @param string str
    *        The string to display.
    */
   _renderString: function(str)
   {
-    if (this.message._quoteStrings) {
-      this.element.textContent = VariablesView.getString(str);
-    } else {
-      this.element.textContent = str;
-    }
+    this.element.textContent = VariablesView.getString(str, {
+      noStringQuotes: !this.message._quoteStrings,
+      noEllipsis: true,
+    });
   },
 
   /**
    * Render the anchor ellipsis that allows the user to expand the long string.
    *
    * @private
    * @return Element
    */
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -59,16 +59,20 @@ support-files =
   test-bug-859170-longstring-hang.html
   test-bug-869003-iframe.html
   test-bug-869003-top-window.html
   test-closures.html
   test-console-assert.html
   test-console-extras.html
   test-console-replaced-api.html
   test-console.html
+  test-console-output-02.html
+  test-console-output-03.html
+  test-console-output-04.html
+  test-console-output-events.html
   test-consoleiframes.html
   test-data.json
   test-data.json^headers^
   test-duplicate-error.html
   test-encoding-ISO-8859-1.html
   test-error.html
   test-eval-in-stackframe.html
   test-file-location.js
@@ -162,17 +166,16 @@ support-files =
 [browser_webconsole_bug_595223_file_uri.js]
 [browser_webconsole_bug_595350_multiple_windows_and_tabs.js]
 [browser_webconsole_bug_595934_message_categories.js]
 [browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js]
 [browser_webconsole_bug_597136_external_script_errors.js]
 [browser_webconsole_bug_597136_network_requests_from_chrome.js]
 [browser_webconsole_bug_597460_filter_scroll.js]
 [browser_webconsole_bug_597756_reopen_closed_tab.js]
-[browser_webconsole_bug_598357_jsterm_output.js]
 [browser_webconsole_bug_599725_response_headers.js]
 [browser_webconsole_bug_600183_charset.js]
 [browser_webconsole_bug_601177_log_levels.js]
 [browser_webconsole_bug_601352_scroll.js]
 [browser_webconsole_bug_601667_filter_buttons.js]
 [browser_webconsole_bug_602572_log_bodies_checkbox.js]
 [browser_webconsole_bug_603750_websocket.js]
 [browser_webconsole_bug_611795.js]
@@ -243,8 +246,13 @@ run-if = os == "mac"
 [browser_webconsole_scratchpad_panel_link.js]
 [browser_webconsole_split.js]
 [browser_webconsole_view_source.js]
 [browser_webconsole_reflow.js]
 [browser_webconsole_log_file_filter.js]
 [browser_webconsole_expandable_timestamps.js]
 [browser_webconsole_autocomplete_in_debugger_stackframe.js]
 [browser_webconsole_autocomplete_popup_close_on_tab_switch.js]
+[browser_webconsole_output_01.js]
+[browser_webconsole_output_02.js]
+[browser_webconsole_output_03.js]
+[browser_webconsole_output_04.js]
+[browser_webconsole_output_events.js]
--- a/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
+++ b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
@@ -31,17 +31,18 @@ function consoleOpened(hud)
 }
 
 function onExecuteFooObj(msg)
 {
   ok(msg, "output message found");
 
   let anchor = msg.querySelector("a");
   ok(anchor, "object anchor");
-  isnot(anchor.textContent.indexOf("[object Object]"), -1, "message text check");
+  isnot(anchor.textContent.indexOf('testProp: "testValue"'), -1,
+        "message text check");
 
   gJSTerm.once("variablesview-fetched", onFooObjFetch);
   EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow)
 }
 
 function onFooObjFetch(aEvent, aVar)
 {
   gVariablesView = aVar._variablesView;
@@ -71,17 +72,18 @@ function onSidebarClosed()
   gJSTerm.execute("window", onExecuteWindow);
 }
 
 function onExecuteWindow(msg)
 {
   ok(msg, "output message found");
   let anchor = msg.querySelector("a");
   ok(anchor, "object anchor");
-  isnot(anchor.textContent.indexOf("[object Window]"), -1, "message text check");
+  isnot(anchor.textContent.indexOf("Window \u2192 http://example.com/browser/"), -1,
+        "message text check");
 
   gJSTerm.once("variablesview-fetched", onWindowFetch);
   EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow)
 }
 
 function onWindowFetch(aEvent, aVar)
 {
   gVariablesView = aVar._variablesView;
--- a/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
+++ b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -41,17 +41,17 @@ function consoleOpened(hud)
     }],
   }).then(onConsoleMessage);
 }
 
 function onConsoleMessage(aResults)
 {
   let clickable = aResults[0].clickableElements[0];
   ok(clickable, "clickable object found");
-  isnot(clickable.textContent.indexOf("[object Object]"), -1,
+  isnot(clickable.textContent.indexOf('{hello: "world!",'), -1,
         "message text check");
 
   gJSTerm.once("variablesview-fetched", onObjFetch);
 
   EventUtils.synthesizeMouse(clickable, 2, 2, {}, gWebConsole.iframeWindow)
 }
 
 function onObjFetch(aEvent, aVar)
--- a/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
+++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
@@ -66,17 +66,17 @@ function test()
         {
           name: "console.warn output",
           text: "bug851231-warn",
           category: CATEGORY_WEBDEV,
           severity: SEVERITY_WARNING,
         },
         {
           name: "console.error output",
-          text: /\bbug851231-error\b.+\[object Object\]/,
+          text: /\bbug851231-error\b.+\{bug851231prop:\s"bug851231value"\}/,
           category: CATEGORY_WEBDEV,
           severity: SEVERITY_ERROR,
           objects: true,
         },
         {
           name: "console.debug output",
           text: "bug851231-debug",
           category: CATEGORY_WEBDEV,
@@ -86,17 +86,17 @@ function test()
           name: "console.trace output",
           consoleTrace: {
             file: "browser_console_consolejsm_output.js",
             fn: "onCachedMessage",
           },
         },
         {
           name: "console.dir output",
-          consoleDir: "[object XULDocument]",
+          consoleDir: "XULDocument {",
         },
         {
           name: "console.time output",
           consoleTime: "foobarTimer",
         },
         {
           name: "console.timeEnd output",
           consoleTimeEnd: "foobarTimer",
--- a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
+++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
@@ -29,17 +29,17 @@ function performTest(hud)
       text: "fooBug676722",
       category: CATEGORY_WEBDEV,
       severity: SEVERITY_LOG,
       objects: true,
     }],
   }).then(([result]) => {
     let clickable = result.clickableElements[0];
     ok(clickable, "the console.log() object anchor was found");
-    isnot(clickable.textContent.indexOf("Object"), -1,
+    isnot(clickable.textContent.indexOf('{abba: "omgBug676722"}'), -1,
           "clickable node content is correct");
 
     hud.jsterm.once("variablesview-fetched",
       (aEvent, aVar) => {
         ok(aVar, "object inspector opened on click");
 
         findVariableViewProperties(aVar, [{
           name: "abba",
--- a/browser/devtools/webconsole/test/browser_console_native_getters.js
+++ b/browser/devtools/webconsole/test/browser_console_native_getters.js
@@ -25,17 +25,17 @@ function consoleOpened(hud)
   gWebConsole = hud;
   gJSTerm = hud.jsterm;
 
   gJSTerm.execute("document");
 
   waitForMessages({
     webconsole: hud,
     messages: [{
-      text: "[object HTMLDocument]",
+      text: "HTMLDocument \u2192 data:text/html;charset=utf8",
       category: CATEGORY_OUTPUT,
       objects: true,
     }],
   }).then(onEvalResult);
 }
 
 function onEvalResult(aResults)
 {
@@ -85,17 +85,17 @@ function onFetchAfterBackgroundUpdate(aE
 
 function testParagraphs()
 {
   gJSTerm.execute("$$('p')");
 
   waitForMessages({
     webconsole: gWebConsole,
     messages: [{
-      text: "[object NodeList]",
+      text: "NodeList [",
       category: CATEGORY_OUTPUT,
       objects: true,
     }],
   }).then(onEvalNodeList);
 }
 
 function onEvalNodeList(aResults)
 {
--- a/browser/devtools/webconsole/test/browser_console_variables_view.js
+++ b/browser/devtools/webconsole/test/browser_console_variables_view.js
@@ -23,17 +23,18 @@ function consoleOpened(hud)
   gWebConsole = hud;
   gJSTerm = hud.jsterm;
   gJSTerm.execute("fooObj", onExecuteFooObj);
 }
 
 function onExecuteFooObj(msg)
 {
   ok(msg, "output message found");
-  isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+  isnot(msg.textContent.indexOf('{testProp: "testValue"}'), -1,
+        "message text check");
 
   let anchor = msg.querySelector("a");
   ok(anchor, "object link found");
 
   gJSTerm.once("variablesview-fetched", onFooObjFetch);
 
   executeSoon(() =>
     EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow)
--- a/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js
@@ -57,17 +57,18 @@ function onFramesAdded()
     )
   );
 }
 
 
 function onExecuteFooObj(msg)
 {
   ok(msg, "output message found");
-  isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+  isnot(msg.textContent.indexOf('{testProp2: "testValue2"}'), -1,
+        "message text check");
 
   let anchor = msg.querySelector("a");
   ok(anchor, "object link found");
 
   gJSTerm.once("variablesview-fetched", onFooObjFetch);
 
   executeSoon(() => EventUtils.synthesizeMouse(anchor, 2, 2, {},
                                                gWebConsole.iframeWindow));
--- a/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging_and_inspecting.js
@@ -52,17 +52,18 @@ function onFramesAdded()
   info("onFramesAdded");
 
   openConsole(null, () => gJSTerm.execute("fooObj", onExecuteFooObj));
 }
 
 function onExecuteFooObj(msg)
 {
   ok(msg, "output message found");
-  isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+  isnot(msg.textContent.indexOf('{testProp2: "testValue2"}'), -1,
+        "message text check");
 
   let anchor = msg.querySelector("a");
   ok(anchor, "object link found");
 
   gJSTerm.once("variablesview-fetched", onFooObjFetch);
 
   EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow);
 }
--- a/browser/devtools/webconsole/test/browser_jsterm_inspect.js
+++ b/browser/devtools/webconsole/test/browser_jsterm_inspect.js
@@ -24,12 +24,12 @@ function test()
   }
 
   function onObjFetch(aEvent, aVar)
   {
     ok(aVar._variablesView, "variables view object");
 
     findVariableViewProperties(aVar, [
       { name: "testProp", value: "testValue" },
-      { name: "document", value: "HTMLDocument" },
+      { name: "document", value: /HTMLDocument \u2192 data:/ },
     ], { webconsole: hud }).then(finishTest);
   }
 }
--- a/browser/devtools/webconsole/test/browser_result_format_as_string.js
+++ b/browser/devtools/webconsole/test/browser_result_format_as_string.js
@@ -23,19 +23,19 @@ function performTest(hud)
   hud.jsterm.clearOutput(true);
 
   hud.jsterm.execute("document.querySelector('p')", (msg) => {
     is(hud.outputNode.textContent.indexOf("bug772506_content"), -1,
        "no content element found");
     ok(!hud.outputNode.querySelector("#foobar"), "no #foobar element found");
 
     ok(msg, "eval output node found");
-    is(msg.textContent.indexOf("HTMLDivElement"), -1,
-       "HTMLDivElement string is not displayed");
-    isnot(msg.textContent.indexOf("HTMLParagraphElement"), -1,
-          "HTMLParagraphElement string is displayed");
+    is(msg.textContent.indexOf("<div>"), -1,
+       "<div> string is not displayed");
+    isnot(msg.textContent.indexOf("<p>"), -1,
+          "<p> string is displayed");
 
     EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"});
     ok(!gBrowser._bug772506, "no content variable");
 
     finishTest();
   });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js
@@ -75,19 +75,18 @@ function tab2Loaded(aEvent) {
       ok(false, "gDevTools.closeToolbox(target1) exception: " + ex);
       noErrors = false;
     }
   }
 
   function testEnd() {
     ok(noErrors, "there were no errors");
 
-    Array.forEach(win1.gBrowser.tabs, function(aTab) {
-      win1.gBrowser.removeTab(aTab);
-    });
+    win1.gBrowser.removeTab(tab1);
+
     Array.forEach(win2.gBrowser.tabs, function(aTab) {
       win2.gBrowser.removeTab(aTab);
     });
 
     executeSoon(function() {
       win2.close();
       tab1 = tab2 = win1 = win2 = null;
       finishTest();
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
@@ -52,42 +52,37 @@ function autocompletePopupHidden()
   let jsterm = gHUD.jsterm;
   let popup = jsterm.autocompletePopup;
   let completeNode = jsterm.completeNode;
   let inputNode = jsterm.inputNode;
 
   popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false);
 
   ok(!popup.isOpen, "popup is not open");
+
+  jsterm.once("autocomplete-updated", function() {
+    is(completeNode.value, testStr + "dy", "autocomplete shows document.body");
+    testPropertyPanel();
+  });
+
   let inputStr = "document.b";
   jsterm.setInputValue(inputStr);
   EventUtils.synthesizeKey("o", {});
   let testStr = inputStr.replace(/./g, " ") + " ";
-
-  waitForSuccess({
-    name: "autocomplete shows document.body",
-    validatorFn: function()
-    {
-      return completeNode.value == testStr + "dy";
-    },
-    successFn: testPropertyPanel,
-    failureFn: finishTest,
-  });
 }
 
 function testPropertyPanel()
 {
   let jsterm = gHUD.jsterm;
   jsterm.clearOutput();
   jsterm.execute("document", (msg) => {
     jsterm.once("variablesview-fetched", onVariablesViewReady);
     let anchor = msg.querySelector(".body a");
     EventUtils.synthesizeMouse(anchor, 2, 2, {}, gHUD.iframeWindow);
   });
 }
 
 function onVariablesViewReady(aEvent, aView)
 {
   findVariableViewProperties(aView, [
-    { name: "body", value: "HTMLBodyElement" },
+    { name: "body", value: "<body>" },
   ], { webconsole: gHUD }).then(finishTest);
 }
-
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -83,18 +83,17 @@ function performWebConsoleTests(hud)
   let jsterm = hud.jsterm;
   outputNode = hud.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("$0", onNodeOutput);
 
   function onNodeOutput(node)
   {
-    isnot(node.textContent.indexOf("[object HTMLHeadingElement"), -1,
-          "correct output for $0");
+    isnot(node.textContent.indexOf("<h1>"), -1, "correct output for $0");
 
     jsterm.clearOutput();
     jsterm.execute("$0.textContent = 'bug653531'", onNodeUpdate);
   }
 
   function onNodeUpdate(node)
   {
     isnot(node.textContent.indexOf("bug653531"), -1,
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -17,13 +17,13 @@ function test() {
 
 function consoleOpened(hud) {
   hud.jsterm.execute("console.dir(document)");
   hud.jsterm.once("variablesview-fetched", testConsoleDir.bind(null, hud));
 }
 
 function testConsoleDir(hud, ev, view) {
   findVariableViewProperties(view, [
-    { name: "__proto__.__proto__.querySelectorAll", value: "Function" },
-    { name: "location", value: "Location" },
-    { name: "__proto__.write", value: "Function" },
+    { name: "__proto__.__proto__.querySelectorAll", value: "querySelectorAll()" },
+    { name: "location", value: /Location \u2192 data:Web/ },
+    { name: "__proto__.write", value: "write()" },
   ], { webconsole: hud }).then(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_closure_inspection.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_closure_inspection.js
@@ -49,17 +49,17 @@ function consoleOpened(hud)
 {
   gWebConsole = hud;
   gJSTerm = hud.jsterm;
   gJSTerm.execute("window.george.getName");
 
   waitForMessages({
     webconsole: gWebConsole,
     messages: [{
-      text: "[object Function]",
+      text: "function _pfactory/<.getName()",
       category: CATEGORY_OUTPUT,
       objects: true,
     }],
   }).then(onExecuteGetName);
 }
 
 function onExecuteGetName(aResults)
 {
--- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
@@ -113,17 +113,17 @@ function testJSTerm(hud)
 
   let output = jsterm.outputNode.querySelector(".message[category='output']");
   ok(!output, "no output for help() calls");
   is(openedLinks, 3, "correct number of pages opened by the help calls");
   hud.openLink = oldOpenLink;
 
   jsterm.clearOutput();
   jsterm.execute("pprint({b:2, a:1})");
-  checkResult('"  b: 2\n  a: 1"', "pprint()");
+  checkResult("\"  b: 2\\n  a: 1\"", "pprint()");
   yield undefined;
 
   // check instanceof correctness, bug 599940
   jsterm.clearOutput();
   jsterm.execute("[] instanceof Array");
   checkResult("true", "[] instanceof Array == true");
   yield undefined;
 
@@ -149,17 +149,17 @@ function testJSTerm(hud)
   jsterm.clearOutput();
   jsterm.execute("keys(window)");
   checkResult(null, "keys(window)");
   yield undefined;
 
   // bug 614561
   jsterm.clearOutput();
   jsterm.execute("pprint('hi')");
-  checkResult('"  0: "h"\n  1: "i""', "pprint('hi')");
+  checkResult("\"  0: \\\"h\\\"\\n  1: \\\"i\\\"\"", "pprint('hi')");
   yield undefined;
 
   // check that pprint(function) shows function source, bug 618344
   jsterm.clearOutput();
   jsterm.execute("pprint(print)");
   checkResult(function(node) {
     return node.textContent.indexOf("aOwner.helperResult") > -1;
   }, "pprint(function) shows source");
rename from browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
rename to browser/devtools/webconsole/test/browser_webconsole_output_01.js
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_01.js
@@ -1,267 +1,146 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
+/*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
- *
- * Contributor(s):
- *  Mihai Șucan <mihai.sucan@gmail.com>
- *
- * ***** END LICENSE BLOCK ***** */
+ */
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+// Test the webconsole output for various types of objects.
 
-let testEnded = false;
-let pos = -1;
+const TEST_URI = "data:text/html;charset=utf8,test for console output - 01";
 
 let dateNow = Date.now();
-
-let tempScope = {};
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+let {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
 
-let longString = (new Array(tempScope.DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
-let initialString = longString.substring(0,
-  tempScope.DebuggerServer.LONG_STRING_INITIAL_LENGTH);
+let LONG_STRING_LENGTH = DebuggerServer.LONG_STRING_LENGTH;
+let LONG_STRING_INITIAL_LENGTH = DebuggerServer.LONG_STRING_INITIAL_LENGTH;
+DebuggerServer.LONG_STRING_LENGTH = 100;
+DebuggerServer.LONG_STRING_INITIAL_LENGTH = 50;
 
-let inputValues = [
-  // [showsVariablesView?, input value, expected output format,
-  //    print() output, console API output, optional console API test]
+let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
+let initialString = longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
 
+let inputTests = [
   // 0
-  [false, "'hello \\nfrom \\rthe \\\"string world!'",
-    '"hello \nfrom \rthe "string world!"',
-    "hello \nfrom \rthe \"string world!"],
+  {
+    input: "'hello \\nfrom \\rthe \\\"string world!'",
+    output: "\"hello \\nfrom \\rthe \\\"string world!\"",
+  },
 
   // 1
-  [false, "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'",
-    "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"",
-    "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165"],
+  {
+    // unicode test
+    input: "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'",
+    output: "\"\\xFA\\u1E47\\u0129\\xE7\\xF6d\\xEA \\u021B\\u0115\\u0219\\u0165\"",
+  },
 
   // 2
-  [false, "window.location.href", '"' + TEST_URI + '"', TEST_URI],
+  {
+    input: "'" + longString + "'",
+    output: '"' + initialString + "\"[\u2026]",
+    printOutput: initialString,
+  },
 
   // 3
-  [false, "0", "0"],
+  {
+    input: "''",
+    output: '""',
+    printOutput: '""',
+  },
 
   // 4
-  [false, "'0'", '"0"', "0"],
+  {
+    input: "0",
+    output: "0",
+  },
 
   // 5
-  [false, "42", "42"],
+  {
+    input: "'0'",
+    output: '"0"',
+  },
 
   // 6
-  [false, "'42'", '"42"', "42"],
+  {
+    input: "42",
+    output: "42",
+  },
 
   // 7
-  [true, "/foobar/", "[object RegExp]", '"/foobar/"', "[object RegExp]"],
+  {
+    input: "'42'",
+    output: '"42"',
+  },
 
   // 8
-  [false, "null", "null"],
+  {
+    input: "/foobar/",
+    output: "/foobar/",
+    inspectable: true,
+  },
 
   // 9
-  [false, "undefined", "undefined"],
+  {
+    input: "/foo?b*\\s\"ar/igym",
+    output: "/foo?b*\\s\"ar/gimy",
+    printOutput: "/foo?b*\\\\s\\\"ar/gimy",
+    inspectable: true,
+  },
 
   // 10
-  [false, "true", "true"],
+  {
+    input: "null",
+    output: "null",
+  },
 
   // 11
-  [true, "document.getElementById", "[object Function]",
-    "function getElementById() {\n    [native code]\n}",
-    "[object Function]"],
+  {
+    input: "undefined",
+    output: "undefined",
+  },
 
   // 12
-  [true, "(function() { return 42; })", "[object Function]",
-    "function () { return 42; }", "[object Function]"],
+  {
+    input: "true",
+    output: "true",
+  },
 
   // 13
-  [true, "new Date(" + dateNow + ")", "[object Date]", (new Date(dateNow)).toString(), "[object Date]"],
+  {
+    input: "false",
+    output: "false",
+  },
+
 
   // 14
-  [true, "document.body", "[object HTMLBodyElement]"],
+  {
+    input: "new Date(" + dateNow + ")",
+    output: "Date " + (new Date(dateNow)).toISOString(),
+    printOutput: (new Date(dateNow)).toString(),
+    inspectable: true,
+  },
 
   // 15
-  [true, "window.location", "[object Location]", TEST_URI, "[object Location]"],
-
-  // 16
-  [true, "[1,2,3,'a','b','c','4','5']", '[object Array]',
-    '1,2,3,a,b,c,4,5',
-    "[object Array]"],
-
-  // 17
-  [true, "({a:'b', c:'d', e:1, f:'2'})", "[object Object]"],
-
-  // 18
-  [false, "'" + longString + "'",
-    '"' + initialString + "\"[\u2026]", initialString],
+  {
+    input: "new Date('test')",
+    output: "Invalid Date",
+    printOutput: "Invalid Date",
+    inspectable: true,
+    variablesViewLabel: "Invalid Date",
+  },
 ];
 
-longString = null;
-initialString = null;
-tempScope = null;
-
-let eventHandlers = [];
-let popupShown = [];
-let HUD;
-let testDriver;
-
-function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, tabLoad, true);
-
-  waitForFocus(function () {
-    openConsole(null, function(aHud) {
-      HUD = aHud;
-      testNext();
-    });
-  }, content);
-}
-
-function subtestNext() {
-  testDriver.next();
-}
-
-function testNext() {
-  pos++;
-  if (pos == inputValues.length) {
-    testEnd();
-    return;
-  }
-
-  testDriver = testGen();
-  testDriver.next();
-}
-
-function testGen() {
-  let cpos = pos;
-
-  let showsVariablesView = inputValues[cpos][0];
-  let inputValue = inputValues[cpos][1];
-  let expectedOutput = inputValues[cpos][2];
-
-  let printOutput = inputValues[cpos].length >= 4 ?
-    inputValues[cpos][3] : expectedOutput;
-
-  let consoleOutput = inputValues[cpos].length >= 5 ?
-    inputValues[cpos][4] : printOutput;
-
-  let consoleTest = inputValues[cpos][5] || inputValue;
-
-  HUD.jsterm.clearOutput();
-
-  // Test the console.log() output.
-
-  let outputItem;
-  function onExecute(msg) {
-    outputItem = msg;
-    subtestNext();
-  }
-
-  HUD.jsterm.execute("console.log(" + consoleTest + ")");
-
-  waitForMessages({
-    webconsole: HUD,
-    messages: [{
-      name: "console API output is correct for inputValues[" + cpos + "]",
-      text: consoleOutput,
-      category: CATEGORY_WEBDEV,
-      severity: SEVERITY_LOG,
-    }],
-  }).then(subtestNext);
-
-  yield undefined;
-
-  HUD.jsterm.clearOutput();
-
-  // Test jsterm print() output.
-
-  HUD.jsterm.setInputValue("print(" + inputValue + ")");
-  HUD.jsterm.execute(null, onExecute);
-
-  yield undefined;
-
-  ok(outputItem,
-    "found the jsterm print() output line for inputValues[" + cpos + "]");
-  ok(outputItem.textContent.indexOf(printOutput) > -1,
-    "jsterm print() output is correct for inputValues[" + cpos + "]");
-
-  // Test jsterm execution output.
-
-  HUD.jsterm.clearOutput();
-  HUD.jsterm.setInputValue(inputValue);
-  HUD.jsterm.execute(null, onExecute);
-
-  yield undefined;
-
-  ok(outputItem, "found the jsterm output line for inputValues[" + cpos + "]");
-  ok(outputItem.textContent.indexOf(expectedOutput) > -1,
-    "jsterm output is correct for inputValues[" + cpos + "]");
-
-  let messageBody = outputItem.querySelector(".body a") ||
-                    outputItem.querySelector(".body");
-  ok(messageBody, "we have the message body for inputValues[" + cpos + "]");
-
-  // Test click on output.
-  let eventHandlerID = eventHandlers.length + 1;
-
-  let variablesViewShown = function(aEvent, aView, aOptions) {
-    if (aOptions.label.indexOf(expectedOutput) == -1) {
-      return;
-    }
-
-    HUD.jsterm.off("variablesview-open", variablesViewShown);
-
-    eventHandlers[eventHandlerID] = null;
-
-    ok(showsVariablesView,
-      "the variables view shown for inputValues[" + cpos + "]");
-
-    popupShown[cpos] = true;
-
-    if (showsVariablesView) {
-      executeSoon(subtestNext);
-    }
-  };
-
-  HUD.jsterm.on("variablesview-open", variablesViewShown);
-
-  eventHandlers.push(variablesViewShown);
-
-  EventUtils.synthesizeMouse(messageBody, 2, 2, {}, HUD.iframeWindow);
-
-  if (showsVariablesView) {
-    info("messageBody tagName '" + messageBody.tagName +  "' className '" + messageBody.className + "'");
-    yield undefined; // wait for the panel to open if we need to.
-  }
-
-  testNext();
-
-  yield undefined;
-}
-
-function testEnd() {
-  if (testEnded) {
-    return;
-  }
-
-  testEnded = true;
-
-  for (let i = 0; i < eventHandlers.length; i++) {
-    if (eventHandlers[i]) {
-      HUD.jsterm.off("variablesview-open", eventHandlers[i]);
-    }
-  }
-
-  for (let i = 0; i < inputValues.length; i++) {
-    if (inputValues[i][0] && !popupShown[i]) {
-      ok(false, "the variables view failed to show for inputValues[" + i + "]");
-    }
-  }
-
-  HUD = inputValues = testDriver = null;
-  executeSoon(finishTest);
-}
+longString = initialString = null;
 
 function test() {
-  requestLongerTimeout(2);
+  registerCleanupFunction(() => {
+    DebuggerServer.LONG_STRING_LENGTH = LONG_STRING_LENGTH;
+    DebuggerServer.LONG_STRING_INITIAL_LENGTH = LONG_STRING_INITIAL_LENGTH;
+  });
+
   addTab(TEST_URI);
-  browser.addEventListener("load", tabLoad, true);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole().then((hud) => {
+      return checkOutputForInputs(hud, inputTests);
+    }).then(finishTest);
+  }, true);
 }
-
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_02.js
@@ -0,0 +1,161 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-02.html";
+
+let inputTests = [
+  // 0 - native named function
+  {
+    input: "document.getElementById",
+    output: "function getElementById()",
+    printOutput: "function getElementById() {\\n    [native code]\\n}",
+    inspectable: true,
+    variablesViewLabel: "getElementById()",
+  },
+
+  // 1 - anonymous function
+  {
+    input: "(function() { return 42; })",
+    output: "function ()",
+    printOutput: "function () { return 42; }",
+    inspectable: true,
+  },
+
+  // 2 - named function
+  {
+    input: "window.testfn1",
+    output: "function testfn1()",
+    printOutput: "function testfn1() { return 42; }",
+    inspectable: true,
+    variablesViewLabel: "testfn1()",
+  },
+
+  // 3 - anonymous function, but spidermonkey gives us an inferred name.
+  {
+    input: "testobj1.testfn2",
+    output: "function testobj1.testfn2()",
+    printOutput: "function () { return 42; }",
+    inspectable: true,
+    variablesViewLabel: "testobj1.testfn2()",
+  },
+
+  // 4 - named function with custom display name
+  {
+    input: "window.testfn3",
+    output: "function testfn3DisplayName()",
+    printOutput: "function testfn3() { return 42; }",
+    inspectable: true,
+    variablesViewLabel: "testfn3DisplayName()",
+  },
+
+  // 5 - basic array
+  {
+    input: "window.array1",
+    output: '[1, 2, 3, "a", "b", "c", "4", "5"]',
+    printOutput: "1,2,3,a,b,c,4,5",
+    inspectable: true,
+    variablesViewLabel: "Array[8]",
+  },
+
+  // 6 - array with objects
+  {
+    input: "window.array2",
+    output: '["a", HTMLDocument \u2192 test-console-output-02.html, <body>, ' +
+            "DOMStringMap[0], DOMTokenList[0]]",
+    printOutput: '"a,[object HTMLDocument],[object HTMLBodyElement],' +
+                 '[object DOMStringMap],"',
+    inspectable: true,
+    variablesViewLabel: "Array[5]",
+  },
+
+  // 7 - array with more than 10 elements
+  {
+    input: "window.array3",
+    output: '[1, Window \u2192 test-console-output-02.html, null, "a", "b", ' +
+            'undefined, false, "", -Infinity, testfn3DisplayName(), 3 more\u2026]',
+    printOutput: '"1,[object Window],,a,b,,false,,-Infinity,' +
+                 'function testfn3() { return 42; },[object Object],foo,bar"',
+    inspectable: true,
+    variablesViewLabel: "Array[13]",
+  },
+
+  // 8 - array with holes and a cyclic reference
+  {
+    input: "window.array4",
+    output: '[,,,,, "test", Array[7]]',
+    printOutput: '",,,,,test,"',
+    inspectable: true,
+    variablesViewLabel: "Array[7]",
+  },
+
+  // 9
+  {
+    input: "window.typedarray1",
+    output: 'Int32Array [1, 287, 8651, 40983, 8754]',
+    printOutput: "[object Int32Array]",
+    inspectable: true,
+    variablesViewLabel: "Int32Array[5]",
+  },
+
+  // 10 - Set with cyclic reference
+  {
+    input: "window.set1",
+    output: 'Set [1, 2, null, Array[13], "a", "b", undefined, <head>, Set[9]]',
+    printOutput: "[object Set]",
+    inspectable: true,
+    variablesViewLabel: "Set[9]",
+  },
+
+  // 11 - Object with cyclic reference and a getter
+  {
+    input: "window.testobj2",
+    output: '{a: "b", c: "d", e: 1, f: "2", foo: Object, bar: Object, ' +
+            "getterTest: Getter}",
+    printOutput: "[object Object]",
+    inspectable: true,
+    variablesViewLabel: "Object",
+  },
+
+  // 12 - Object with more than 10 properties
+  {
+    input: "window.testobj3",
+    output: '{a: "b", c: "d", e: 1, f: "2", g: true, h: null, i: undefined, ' +
+            'j: "", k: StyleSheetList[0], l: NodeList[5], 2 more\u2026}',
+    printOutput: "[object Object]",
+    inspectable: true,
+    variablesViewLabel: "Object",
+  },
+
+  // 13 - Object with a non-enumerable property that we do not show
+  {
+    input: "window.testobj4",
+    output: '{a: "b", c: "d", 1 more\u2026}',
+    printOutput: "[object Object]",
+    inspectable: true,
+    variablesViewLabel: "Object",
+  },
+
+  // 14 - Map with cyclic references
+  {
+    input: "window.map1",
+    output: 'Map {a: "b", HTMLCollection[2]: Object, Map[3]: Set[9]}',
+    printOutput: "[object Map]",
+    inspectable: true,
+    variablesViewLabel: "Map[3]",
+  },
+];
+
+function test() {
+
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole().then((hud) => {
+      return checkOutputForInputs(hud, inputTests);
+    }).then(finishTest);
+  }, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_03.js
@@ -0,0 +1,164 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-03.html";
+
+let inputTests = [
+  // 0
+  {
+    input: "document",
+    output: "HTMLDocument \u2192 " + TEST_URI,
+    printOutput: "[object HTMLDocument]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 1
+  {
+    input: "window",
+    output: "Window \u2192 " + TEST_URI,
+    printOutput: "[object Window",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 2
+  {
+    input: "document.body",
+    output: "<body>",
+    printOutput: "[object HTMLBodyElement]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 3
+  {
+    input: "document.body.dataset",
+    output: "DOMStringMap {}",
+    printOutput: "[object DOMStringMap]",
+    inspectable: true,
+    variablesViewLabel: "DOMStringMap[0]",
+  },
+
+  // 4
+  {
+    input: "document.body.classList",
+    output: "DOMTokenList []",
+    printOutput: '""',
+    inspectable: true,
+    variablesViewLabel: "DOMTokenList[0]",
+  },
+
+  // 5
+  {
+    input: "window.location.href",
+    output: '"' + TEST_URI + '"',
+  },
+
+  // 6
+  {
+    input: "window.location",
+    output: "Location \u2192 " + TEST_URI,
+    printOutput: TEST_URI,
+    inspectable: true,
+    variablesViewLabel: "Location \u2192 test-console-output-03.html",
+  },
+
+  // 7
+  {
+    input: "document.body.attributes",
+    output: "MozNamedAttrMap []",
+    printOutput: "[object MozNamedAttrMap]",
+    inspectable: true,
+    variablesViewLabel: "MozNamedAttrMap[0]",
+  },
+
+  // 8
+  {
+    input: "document.styleSheets",
+    output: "StyleSheetList []",
+    printOutput: "[object StyleSheetList",
+    inspectable: true,
+    variablesViewLabel: "StyleSheetList[0]",
+  },
+
+  // 9
+  {
+    input: "testBodyClassName()",
+    output: '<body class="test1 tezt2">',
+    printOutput: "[object HTMLBodyElement]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 10
+  {
+    input: "testBodyID()",
+    output: '<body class="test1 tezt2" id="foobarid">',
+    printOutput: "[object HTMLBodyElement]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 11
+  {
+    input: "document.body.classList",
+    output: 'DOMTokenList ["test1", "tezt2"]',
+    printOutput: '"test1 tezt2"',
+    inspectable: true,
+    variablesViewLabel: "DOMTokenList[2]",
+  },
+
+  // 12
+  {
+    input: "testBodyDataset()",
+    output: '<body class="test1 tezt2" id="foobarid"' +
+            ' data-preview="zuzu&quot;&lt;a&gt;foo">',
+    printOutput: "[object HTMLBodyElement]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 13
+  {
+    input: "document.body.dataset",
+    output: 'DOMStringMap {preview: "zuzu\\"<a>foo"}',
+    printOutput: "[object DOMStringMap]",
+    inspectable: true,
+    variablesViewLabel: "DOMStringMap[1]",
+  },
+
+  // 14
+  {
+    input: "document.body.attributes",
+    output: 'MozNamedAttrMap [class="test1 tezt2", id="foobarid", ' +
+            'data-preview="zuzu&quot;&lt;a&gt;foo"]',
+    printOutput: "[object MozNamedAttrMap]",
+    inspectable: true,
+    variablesViewLabel: "MozNamedAttrMap[3]",
+  },
+
+  // 15
+  {
+    input: "document.body.attributes[0]",
+    output: 'class="test1 tezt2"',
+    printOutput: "[object Attr]",
+    inspectable: true,
+    variablesViewLabel: 'class="test1 tezt2"',
+  },
+];
+
+function test() {
+
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole().then((hud) => {
+      return checkOutputForInputs(hud, inputTests);
+    }).then(finishTest);
+  }, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_04.js
@@ -0,0 +1,120 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test the webconsole output for various types of objects.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-04.html";
+
+let inputTests = [
+  // 0
+  {
+    input: "testTextNode()",
+    output: '#text "hello world!"',
+    printOutput: "[object Text]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 1
+  {
+    input: "testCommentNode()",
+    output: "<!--\n  - Any copyright ",
+    printOutput: "[object Comment]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 2
+  {
+    input: "testDocumentFragment()",
+    output: 'DocumentFragment [<div id="foo1" class="bar">, <div id="foo3">]',
+    printOutput: "[object DocumentFragment]",
+    inspectable: true,
+    variablesViewLabel: "DocumentFragment[2]",
+  },
+
+  // 3
+  {
+    input: "testError()",
+    output: "TypeError: window.foobar is not a function\n" +
+            "Stack trace:\n" +
+            "testError@" + TEST_URI + ":44",
+    printOutput: '"TypeError: window.foobar is not a function"',
+    inspectable: true,
+    variablesViewLabel: "TypeError",
+  },
+
+  // 4
+  {
+    input: "testDOMException()",
+    output: 'DOMException [SyntaxError: "An invalid or illegal string was specified"',
+    printOutput: '[Exception... \\"An invalid or illegal string was specified\\"',
+    inspectable: true,
+    variablesViewLabel: "SyntaxError",
+  },
+
+  // 5
+  {
+    input: "testCSSStyleDeclaration()",
+    output: 'CSS2Properties {color: "green", font-size: "2em"}',
+    printOutput: "[object CSS2Properties]",
+    inspectable: true,
+    noClick: true,
+  },
+
+  // 6
+  {
+    input: "testStyleSheetList()",
+    output: "StyleSheetList [CSSStyleSheet]",
+    printOutput: "[object StyleSheetList",
+    inspectable: true,
+    variablesViewLabel: "StyleSheetList[1]",
+  },
+
+  // 7
+  {
+    input: "document.styleSheets[0]",
+    output: "CSSStyleSheet",
+    printOutput: "[object CSSStyleSheet]",
+    inspectable: true,
+  },
+
+  // 8
+  {
+    input: "document.styleSheets[0].cssRules",
+    output: "CSSRuleList [CSSStyleRule, CSSMediaRule]",
+    printOutput: "[object CSSRuleList",
+    inspectable: true,
+    variablesViewLabel: "CSSRuleList[2]",
+  },
+
+  // 9
+  {
+    input: "document.styleSheets[0].cssRules[0]",
+    output: 'CSSStyleRule "p, div"',
+    printOutput: "[object CSSStyleRule",
+    inspectable: true,
+    variablesViewLabel: "CSSStyleRule",
+  },
+
+  // 10
+  {
+    input: "document.styleSheets[0].cssRules[1]",
+    output: 'CSSMediaRule "print"',
+    printOutput: "[object CSSMediaRule",
+    inspectable: true,
+    variablesViewLabel: "CSSMediaRule",
+  },
+];
+
+function test() {
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole().then((hud) => {
+      return checkOutputForInputs(hud, inputTests);
+    }).then(finishTest);
+  }, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_events.js
@@ -0,0 +1,60 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test the webconsole output for DOM events.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-events.html";
+
+function test() {
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    Task.spawn(runner);
+  }, true);
+
+  function* runner()
+  {
+    let hud = yield openConsole();
+
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("testDOMEvents()");
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "testDOMEvents() output",
+        text: "undefined",
+        category: CATEGORY_OUTPUT,
+      }],
+    });
+
+    EventUtils.synthesizeMouse(content.document.body, 2, 2, {type: "mousemove"}, content);
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "console.log() output for mousemove",
+        text: /"eventLogger" mousemove {target: .+, buttons: 1, clientX: \d+, clientY: \d+, layerX: \d+, layerY: \d+}/,
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      }],
+    });
+
+    content.focus();
+    EventUtils.synthesizeKey("a", {shiftKey: true}, content);
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "console.log() output for keypress",
+        text: /"eventLogger" keypress Shift {target: .+, key: .+, charCode: \d+, keyCode: \d+}/,
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      }],
+    });
+
+    finishTest();
+  }
+}
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -1,25 +1,25 @@
 /* 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/. */
 
-let WebConsoleUtils, gDevTools, TargetFactory, console, promise, require;
+let WebConsoleUtils, TargetFactory, require;
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+let {Promise: promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 
 (() => {
-  gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools;
-  console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console;
-  promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
-
-  let tools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
-  let utils = tools.require("devtools/toolkit/webconsole/utils");
-  TargetFactory = tools.TargetFactory;
+  let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+  let utils = devtools.require("devtools/toolkit/webconsole/utils");
+  TargetFactory = devtools.TargetFactory;
   WebConsoleUtils = utils.Utils;
-  require = tools.require;
+  require = devtools.require;
 })();
 // promise._reportErrors = true; // please never leave me.
 
 let gPendingOutputTest = 0;
 
 // The various categories of messages.
 const CATEGORY_NETWORK = 0;
 const CATEGORY_CSS = 1;
@@ -147,25 +147,30 @@ function findLogEntry(aString)
  * Open the Web Console for the given tab.
  *
  * @param nsIDOMElement [aTab]
  *        Optional tab element for which you want open the Web Console. The
  *        default tab is taken from the global variable |tab|.
  * @param function [aCallback]
  *        Optional function to invoke after the Web Console completes
  *        initialization (web-console-created).
+ * @return object
+ *         A promise that is resolved once the web console is open.
  */
 function openConsole(aTab, aCallback = function() { })
 {
+  let deferred = promise.defer();
   let target = TargetFactory.forTab(aTab || tab);
   gDevTools.showToolbox(target, "webconsole").then(function(toolbox) {
     let hud = toolbox.getCurrentPanel().hud;
     hud.jsterm._lazyVariablesView = false;
     aCallback(hud);
+    deferred.resolve(hud);
   });
+  return deferred.promise;
 }
 
 /**
  * Close the Web Console for the given tab.
  *
  * @param nsIDOMElement [aTab]
  *        Optional tab element for which you want close the Web Console. The
  *        default tab is taken from the global variable |tab|.
@@ -1254,8 +1259,158 @@ function whenDelayedStartupFinished(aWin
 {
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
+
+/**
+ * Check the web console output for the given inputs. Each input is checked for
+ * the expected JS eval result, the result of calling print(), the result of
+ * console.log(). The JS eval result is also checked if it opens the variables
+ * view on click.
+ *
+ * @param object hud
+ *        The web console instance to work with.
+ * @param array inputTests
+ *        An array of input tests. An input test element is an object. Each
+ *        object has the following properties:
+ *        - input: string, JS input value to execute.
+ *
+ *        - output: string|RegExp, expected JS eval result.
+ *
+ *        - inspectable: boolean, when true, the test runner expects the JS eval
+ *        result is an object that can be clicked for inspection.
+ *
+ *        - noClick: boolean, when true, the test runner does not click the JS
+ *        eval result. Some objects, like |window|, have a lot of properties and
+ *        opening vview for them is very slow (they can cause timeouts in debug
+ *        builds).
+ *
+ *        - printOutput: string|RegExp, optional, expected output for
+ *        |print(input)|. If this is not provided, printOutput = output.
+ *
+ *        - variablesViewLabel: string|RegExp, optional, the expected variables
+ *        view label when the object is inspected. If this is not provided, then
+ *        |output| is used.
+ */
+function checkOutputForInputs(hud, inputTests)
+{
+  let eventHandlers = new Set();
+
+  function* runner()
+  {
+    for (let [i, entry] of inputTests.entries()) {
+      info("checkInput(" + i + "): " + entry.input);
+      yield checkInput(entry);
+    }
+
+    for (let fn of eventHandlers) {
+      hud.jsterm.off("variablesview-open", fn);
+    }
+  }
+
+  function* checkInput(entry)
+  {
+    yield checkConsoleLog(entry);
+    yield checkPrintOutput(entry);
+    yield checkJSEval(entry);
+  }
+
+  function checkConsoleLog(entry)
+  {
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("console.log(" + entry.input + ")");
+
+    return waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "console.log() output: " + entry.output,
+        text: entry.output,
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      }],
+    });
+  }
+
+  function checkPrintOutput(entry)
+  {
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("print(" + entry.input + ")");
+
+    let printOutput = entry.printOutput || entry.output;
+
+    return waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "print() output: " + printOutput,
+        text: printOutput,
+        category: CATEGORY_OUTPUT,
+      }],
+    });
+  }
+
+  function* checkJSEval(entry)
+  {
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute(entry.input);
+
+    let [result] = yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "JS eval output: " + entry.output,
+        text: entry.output,
+        category: CATEGORY_OUTPUT,
+      }],
+    });
+
+    if (!entry.noClick) {
+      let msg = [...result.matched][0];
+      yield checkObjectClick(entry, msg);
+    }
+  }
+
+  function checkObjectClick(entry, msg)
+  {
+    let body = msg.querySelector(".body a") || msg.querySelector(".body");
+    ok(body, "the message body");
+
+    let deferred = promise.defer();
+
+    entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferred);
+    hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
+    eventHandlers.add(entry._onVariablesViewOpen);
+
+    body.scrollIntoView();
+    EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow);
+
+    if (entry.inspectable) {
+      info("message body tagName '" + body.tagName +  "' className '" + body.className + "'");
+      return deferred.promise; // wait for the panel to open if we need to.
+    }
+
+    return promise.resolve(null);
+  }
+
+  function onVariablesViewOpen(entry, deferred, event, view, options)
+  {
+    let label = entry.variablesViewLabel || entry.output;
+    if (typeof label == "string" && options.label != label) {
+      return;
+    }
+    if (label instanceof RegExp && !label.test(options.label)) {
+      return;
+    }
+
+    hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen);
+    eventHandlers.delete(entry._onVariablesViewOpen);
+    entry._onVariablesViewOpen = null;
+
+    ok(entry.inspectable, "variables view was shown");
+
+    deferred.resolve(null);
+  }
+
+  return Task.spawn(runner);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-output-02.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+  <meta charset="utf-8">
+  <title>Test the web console output - 02</title>
+  <!--
+  - Any copyright is dedicated to the Public Domain.
+  - http://creativecommons.org/publicdomain/zero/1.0/
+  -->
+</head>
+<body>
+  <p>hello world!</p>
+  <script type="text/javascript">
+function testfn1() { return 42; }
+
+var testobj1 = {
+  testfn2: function() { return 42; },
+};
+
+function testfn3() { return 42; }
+testfn3.displayName = "testfn3DisplayName";
+
+var array1 = [1, 2, 3, "a", "b", "c", "4", "5"];
+
+var array2 = ["a", document, document.body, document.body.dataset,
+              document.body.classList];
+
+var array3 = [1, window, null, "a", "b", undefined, false, "", -Infinity, testfn3, testobj1, "foo", "bar"];
+
+var array4 = new Array(5);
+array4.push("test");
+array4.push(array4);
+
+var typedarray1 = new Int32Array([1, 287, 8651, 40983, 8754]);
+
+var set1 = new Set([1, 2, null, array3, "a", "b", undefined, document.head]);
+set1.add(set1);
+
+var testobj2 = {a: "b", c: "d", e: 1, f: "2"};
+testobj2.foo = testobj1;
+testobj2.bar = testobj2;
+Object.defineProperty(testobj2, "getterTest", {
+  enumerable: true,
+  get: function() {
+    return 42;
+  },
+});
+
+var testobj3 = {a: "b", c: "d", e: 1, f: "2", g: true, h: null, i: undefined,
+                j: "", k: document.styleSheets, l: document.body.childNodes,
+                o: new Array(125), m: document.head};
+
+var testobj4 = {a: "b", c: "d"};
+Object.defineProperty(testobj4, "nonEnumerable", { value: "hello world" });
+
+var map1 = new Map([["a", "b"], [document.body.children, testobj2]]);
+map1.set(map1, set1);
+
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-output-03.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+  <meta charset="utf-8">
+  <title>Test the web console output - 03</title>
+  <!--
+  - Any copyright is dedicated to the Public Domain.
+  - http://creativecommons.org/publicdomain/zero/1.0/
+  -->
+</head>
+<body>
+  <p>hello world!</p>
+  <script type="text/javascript">
+function testBodyClassName() {
+  document.body.className = "test1 tezt2";
+  return document.body;
+}
+
+function testBodyID() {
+  document.body.id = 'foobarid';
+  return document.body;
+}
+
+function testBodyDataset() {
+  document.body.dataset.preview = 'zuzu"<a>foo';
+  return document.body;
+}
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-output-04.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+  <meta charset="utf-8">
+  <title>Test the web console output - 04</title>
+  <!--
+  - Any copyright is dedicated to the Public Domain.
+  - http://creativecommons.org/publicdomain/zero/1.0/
+  -->
+</head>
+<body>
+  <p>hello world!</p>
+  <script type="text/javascript">
+function testTextNode() {
+  return document.querySelector("p").childNodes[0];
+}
+
+function testCommentNode() {
+  return document.head.childNodes[5];
+}
+
+function testDocumentFragment() {
+  var frag = document.createDocumentFragment();
+
+  var div = document.createElement("div");
+  div.id = "foo1";
+  div.className = "bar";
+  frag.appendChild(div);
+
+  var span = document.createElement("span");
+  span.id = "foo2";
+  span.textContent = "hello world";
+  div.appendChild(span);
+
+  var div2 = document.createElement("div");
+  div2.id = "foo3";
+  frag.appendChild(div2);
+
+  return frag;
+}
+
+function testError() {
+  try {
+    window.foobar("a");
+  } catch (ex) {
+    return ex;
+  }
+  return null;
+}
+
+function testDOMException() {
+  try {
+    var foo = document.querySelector("foo;()bar!");
+  } catch (ex) {
+    return ex;
+  }
+  return null;
+}
+
+function testCSSStyleDeclaration() {
+  document.body.style = 'color: green; font-size: 2em';
+  return document.body.style;
+}
+
+function testStyleSheetList() {
+  var style = document.querySelector("style");
+  if (!style) {
+    style = document.createElement("style");
+    style.textContent = "p, div { color: blue; font-weight: bold }\n" +
+                        "@media print { p { background-color: yellow } }";
+    document.head.appendChild(style);
+  }
+  return document.styleSheets;
+}
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-output-events.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en-US">
+<head>
+  <meta charset="utf-8">
+  <title>Test the web console output for DOM events</title>
+  <!--
+  - Any copyright is dedicated to the Public Domain.
+  - http://creativecommons.org/publicdomain/zero/1.0/
+  -->
+</head>
+<body>
+  <p>hello world!</p>
+  <script type="text/javascript">
+function testDOMEvents() {
+  function eventLogger(ev) {
+    console.log("eventLogger", ev);
+  }
+  document.addEventListener("mousemove", eventLogger);
+  document.addEventListener("keypress", eventLogger);
+}
+  </script>
+</body>
+</html>
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -1178,20 +1178,16 @@ WebConsoleFrame.prototype = {
         node = msg.init(this.output).render().element;
         break;
       }
       case "dir": {
         body = { arguments: args };
         let clipboardArray = [];
         args.forEach((aValue) => {
           clipboardArray.push(VariablesView.getString(aValue));
-          if (aValue && typeof aValue == "object" &&
-              aValue.type == "longString") {
-            clipboardArray.push(l10n.getStr("longStringEllipsis"));
-          }
         });
         clipboardText = clipboardArray.join(" ");
         break;
       }
 
       case "trace": {
         let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename);
         let functionName = aMessage.functionName ||
@@ -3098,17 +3094,17 @@ JSTerm.prototype = {
         case "inspectObject":
           if (aAfterMessage) {
             if (!aAfterMessage._objectActors) {
               aAfterMessage._objectActors = new Set();
             }
             aAfterMessage._objectActors.add(helperResult.object.actor);
           }
           this.openVariablesView({
-            label: VariablesView.getString(helperResult.object),
+            label: VariablesView.getString(helperResult.object, { concise: true }),
             objectActor: helperResult.object,
           });
           break;
         case "error":
           try {
             errorMessage = l10n.getStr(helperResult.message);
           }
           catch (ex) {
@@ -4260,16 +4256,17 @@ JSTerm.prototype = {
     }
     else if (completionType == this.COMPLETE_BACKWARD) {
       popup.selectPreviousItem();
     }
     else if (completionType == this.COMPLETE_FORWARD) {
       popup.selectNextItem();
     }
 
+    this.emit("autocomplete-updated");
     aCallback && aCallback(this);
   },
 
   onAutocompleteSelect: function JSTF_onAutocompleteSelect()
   {
     // Render the suggestion only if the cursor is at the end of the input.
     if (this.inputNode.selectionStart != this.inputNode.value.length) {
       return;
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -197,18 +197,18 @@ breakpointMenuItem.deleteAll=Remove all 
 # editor when the loading process has started but there is no file to display
 # yet.
 loadingText=Loading\u2026
 
 # LOCALIZATION NOTE (errorLoadingText): The text that is displayed in the debugger
 # viewer when there is an error loading a file
 errorLoadingText=Error loading source:\n
 
-# LOCALIZATION NOTE (emptyStackText): The text that is displayed in the watch
-# expressions list to add a new item.
+# LOCALIZATION NOTE (addWatchExpressionText): The text that is displayed in the
+# watch expressions list to add a new item.
 addWatchExpressionText=Add watch expression
 
 # LOCALIZATION NOTE (emptyVariablesText): The text that is displayed in the
 # variables pane when there are no variables to display.
 emptyVariablesText=No variables to display
 
 # LOCALIZATION NOTE (scopeLabel): The text that is displayed in the variables
 # pane as a header for each variable scope (e.g. "Global scope, "With scope",
@@ -220,16 +220,29 @@ scopeLabel=%S scope
 # the watch expressions scope.
 watchExpressionsScopeLabel=Watch expressions
 
 # LOCALIZATION NOTE (globalScopeLabel): The name of the global scope. This text
 # is added to scopeLabel and displayed in the variables pane as a header for
 # the global scope.
 globalScopeLabel=Global
 
+# LOCALIZATION NOTE (variablesViewErrorStacktrace): This is the text that is
+# shown before the stack trace in an error.
+variablesViewErrorStacktrace=Stack trace:
+
+# LOCALIZATION NOTE (variablesViewMoreObjects): the text that is displayed
+# when you have an object preview that does not show all of the elements. At the end of the list
+# you see "N more..." in the web console output.
+# This is a semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of remaining items in the object
+# example: 3 more…
+variablesViewMoreObjects=#1 more…;#1 more…
+
 # LOCALIZATION NOTE (variablesEditableNameTooltip): The text that is displayed
 # in the variables list on an item with an editable name.
 variablesEditableNameTooltip=Double click to edit
 
 # LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
 # in the variables list on an item with an editable value.
 variablesEditableValueTooltip=Click to change value
 
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -95,21 +95,36 @@ XPCOMUtils.defineLazyGetter(this, "PALET
 XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() {
   let result = [];
   for (let [, buttons] of Iterator(DEFAULT_TOOLBAR_PLACEMENTS)) {
     result = result.concat(buttons);
   }
   return result;
 });
 
-const ALL_BUILTIN_ITEMS = [
-  "fullscreen-button",
-  "switch-to-metro-button",
-  "bookmarks-menu-button",
-];
+XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() {
+  // These special cases are for click events on built-in items that are
+  // contained within customizable items (like the navigation widget).
+  const SPECIAL_CASES = [
+    "back-button",
+    "forward-button",
+    "urlbar-stop-button",
+    "urlbar-go-button",
+    "urlbar-reload-button",
+    "searchbar",
+    "cut-button",
+    "copy-button",
+    "paste-button",
+    "zoom-out-button",
+    "zoom-reset-button",
+    "zoom-in-button",
+  ]
+  return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
+                      .concat(SPECIAL_CASES);
+});
 
 const OTHER_MOUSEUP_MONITORED_ITEMS = [
   "PlacesChevron",
   "PlacesToolbarItems",
 ];
 
 this.BrowserUITelemetry = {
   init: function() {
@@ -313,16 +328,24 @@ this.BrowserUITelemetry = {
     }
 
     // Perhaps we're seeing one of the default toolbar items
     // being clicked.
     if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
       // Base case - we clicked directly on one of our built-in items,
       // and we can go ahead and register that click.
       this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
+      return;
+    }
+
+    // If not, we need to check if one of the ancestors of the clicked
+    // item is in our list of built-in items to check.
+    let candidate = getIDBasedOnFirstIDedAncestor(item);
+    if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
+      this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
     }
   },
 
   _getWindowMeasurements: function(aWindow) {
     let document = aWindow.document;
     let result = {};
 
     // Determine if the Bookmarks bar is currently visible
@@ -391,9 +414,9 @@ function getIDBasedOnFirstIDedAncestor(a
   while (!aNode.id) {
     aNode = aNode.parentNode;
     if (!aNode) {
       return null;
     }
   }
 
   return aNode.id;
-}
+}
\ No newline at end of file
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -46,17 +46,17 @@
 .devtools-toolbarbutton:not([label]) {
   min-width: 32px;
 }
 
 .devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
   display: none;
 }
 
-.devtools-toolbarbutton:not([checked=true]):hover:active {
+.devtools-toolbarbutton:not([checked]):hover:active {
   border-color: hsla(210,8%,5%,.6);
   background: linear-gradient(hsla(220,6%,10%,.3), hsla(212,7%,57%,.15) 65%, hsla(212,7%,57%,.3));
   box-shadow: 0 0 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
 }
 
 .devtools-menulist[open=true],
 .devtools-toolbarbutton[open=true],
 .devtools-toolbarbutton[checked=true] {
@@ -277,38 +277,38 @@
 .devtools-sidebar-tabs > tabs > tab:hover {
   background-image: linear-gradient(hsla(206,37%,4%,.2), hsla(206,37%,4%,.2)), @smallSeparator@;
 }
 
 .devtools-sidebar-tabs > tabs > tab:hover:active {
   background-image: linear-gradient(hsla(206,37%,4%,.4), hsla(206,37%,4%,.4)), @smallSeparator@;
 }
 
-.devtools-sidebar-tabs > tabs > tab[selected=true] + tab {
+.devtools-sidebar-tabs > tabs > tab[selected] + tab {
   background-image: linear-gradient(transparent, transparent), @solidSeparator@;
 }
 
-.devtools-sidebar-tabs > tabs > tab[selected=true] + tab:hover {
+.devtools-sidebar-tabs > tabs > tab[selected] + tab:hover {
   background-image: linear-gradient(hsla(206,37%,4%,.2), hsla(206,37%,4%,.2)), @solidSeparator@;
 }
 
-.devtools-sidebar-tabs > tabs > tab[selected=true] + tab:hover:active {
+.devtools-sidebar-tabs > tabs > tab[selected] + tab:hover:active {
   background-image: linear-gradient(hsla(206,37%,4%,.4), hsla(206,37%,4%,.4)), @solidSeparator@;
 }
 
-.devtools-sidebar-tabs > tabs > tab[selected=true] {
+.devtools-sidebar-tabs > tabs > tab[selected] {
   color: #f5f7fa;
   background-image: linear-gradient(#1d4f73, #1d4f73), @solidSeparator@;
 }
 
-.devtools-sidebar-tabs > tabs > tab[selected=true]:hover {
+.devtools-sidebar-tabs > tabs > tab[selected]:hover {
   background-image: linear-gradient(#274f64, #274f64), @solidSeparator@;
 }
 
-.devtools-sidebar-tabs > tabs > tab[selected=true]:hover:active {
+.devtools-sidebar-tabs > tabs > tab[selected]:hover:active {
   background-image: linear-gradient(#1f3e4f, #1f3e4f), @solidSeparator@;
 }
 
 /* Toolbox - moved from toolbox.css.
  * Rules that apply to the global toolbox like command buttons,
  * devtools tabs, docking buttons, etc. */
 
 #toolbox-controls {
@@ -515,63 +515,63 @@
   white-space: nowrap;
 }
 
 .devtools-tab:hover > image {
   opacity: 0.8;
 }
 
 .devtools-tab:active > image,
-.devtools-tab[selected=true] > image {
+.devtools-tab[selected] > image {
   opacity: 1;
 }
 
 .devtools-tab:hover {
   background-color: hsla(206,37%,4%,.2);
   color: #ced3d9;
 }
 
 .devtools-tab:hover:active {
   background-color: hsla(206,37%,4%,.4);
   color: #f5f7fa;
 }
 
-#toolbox-tabs .devtools-tab[selected=true] {
+#toolbox-tabs .devtools-tab[selected] {
   color: #f5f7fa;
   background-color: #1a4666;
   box-shadow: 0 2px 0 #d7f1ff inset,
               0 8px 3px -5px #2b82bf inset,
               0 -2px 0 rgba(0,0,0,.2) inset;
 }
 
-.devtools-tab[selected=true]:not(:first-child),
-.devtools-tab.highlighted:not(:first-child) {
+.devtools-tab[selected]:not(:first-child),
+.devtools-tab[highlighted]:not(:first-child) {
   border-width: 0;
   -moz-padding-start: 1px;
 }
 
-.devtools-tab[selected=true]:last-child,
-.devtools-tab.highlighted:last-child {
+.devtools-tab[selected]:last-child,
+.devtools-tab[highlighted]:last-child {
   -moz-padding-end: 1px;
 }
 
-.devtools-tab[selected=true] + .devtools-tab,
-.devtools-tab.highlighted + .devtools-tab {
+.devtools-tab[selected] + .devtools-tab,
+.devtools-tab[highlighted] + .devtools-tab {
   -moz-border-start-width: 0;
   -moz-padding-start: 1px;
 }
 
-.devtools-tab:not([selected=true]).highlighted {
+.devtools-tab:not([selected])[highlighted] {
   color: #f5f7fa;
   background-color: hsla(99,100%,14%,.2);
   box-shadow: 0 2px 0 #7bc107 inset;
 }
 
-.devtools-tab:not(.highlighted) > .highlighted-icon,
-.devtools-tab[selected=true] > .highlighted-icon,
-.devtools-tab:not([selected=true]).highlighted > .default-icon {
+.devtools-tab:not([highlighted]) > .highlighted-icon,
+.devtools-tab[selected] > .highlighted-icon,
+.devtools-tab:not([selected])[highlighted] > .default-icon {
   visibility: collapse;
 }
 
 .hidden-labels-box:not(.visible) > label,
 .hidden-labels-box.visible ~ .hidden-labels-box > label:last-child {
   display: none;
 }
--- a/docshell/base/nsIMarkupDocumentViewer.idl
+++ b/docshell/base/nsIMarkupDocumentViewer.idl
@@ -18,17 +18,17 @@ interface nsIDOMNode;
 template<class T> class nsCOMPtr;
 template<class T> class nsTArray;
 %}
 
 interface nsIMarkupDocumentViewer;
 
 [ref] native nsIMarkupDocumentViewerTArray(nsTArray<nsCOMPtr<nsIMarkupDocumentViewer> >);
 
-[scriptable, uuid(3528324f-f5d3-4724-bd8d-9233a7114112)]
+[scriptable, uuid(7aea9561-5346-401c-b40e-418688da2d0d)]
 interface nsIMarkupDocumentViewer : nsISupports
 {
 
 	/*
 	Scrolls to a given DOM content node. 
 	*/
 	void scrollToNode(in nsIDOMNode node);
 
@@ -77,16 +77,28 @@ interface nsIMarkupDocumentViewer : nsIS
    * Set the maximum line width for the document.
    * NOTE: This will generate a reflow!
    *
    * @param maxLineWidth The maximum width of any line boxes on the page,
    *        in CSS pixels.
    */
   void changeMaxLineBoxWidth(in int32_t maxLineBoxWidth);
 
+  /**
+   * Instruct the refresh driver to discontinue painting until further
+   * notice.
+   */
+  void pausePainting();
+
+  /**
+   * Instruct the refresh driver to resume painting after a previous call to
+   * pausePainting().
+   */
+  void resumePainting();
+
   /*
    * Render the document as if being viewed on a device with the specified
    * media type. This will cause a reflow.
    *
    * @param mediaType The media type to be emulated
    */
   void emulateMedium(in AString aMediaType);
 
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -2696,16 +2696,27 @@ nsDocumentViewer::CallChildren(CallChild
 }
 
 struct LineBoxInfo
 {
   nscoord mMaxLineBoxWidth;
 };
 
 static void
+ChangeChildPaintingEnabled(nsIMarkupDocumentViewer* aChild, void* aClosure)
+{
+  bool* enablePainting = (bool*) aClosure;
+  if (*enablePainting) {
+    aChild->ResumePainting();
+  } else {
+    aChild->PausePainting();
+  }
+}
+
+static void
 ChangeChildMaxLineBoxWidth(nsIMarkupDocumentViewer* aChild, void* aClosure)
 {
   struct LineBoxInfo* lbi = (struct LineBoxInfo*) aClosure;
   aChild->ChangeMaxLineBoxWidth(lbi->mMaxLineBoxWidth);
 }
 
 struct ZoomInfo
 {
@@ -3121,17 +3132,46 @@ AppendChildSubtree(nsIMarkupDocumentView
 
 NS_IMETHODIMP nsDocumentViewer::AppendSubtree(nsTArray<nsCOMPtr<nsIMarkupDocumentViewer> >& aArray)
 {
   aArray.AppendElement(this);
   CallChildren(AppendChildSubtree, &aArray);
   return NS_OK;
 }
 
-NS_IMETHODIMP nsDocumentViewer::ChangeMaxLineBoxWidth(int32_t aMaxLineBoxWidth)
+NS_IMETHODIMP
+nsDocumentViewer::PausePainting()
+{
+  bool enablePaint = false;
+  CallChildren(ChangeChildPaintingEnabled, &enablePaint);
+
+  nsIPresShell* presShell = GetPresShell();
+  if (presShell) {
+    presShell->PausePainting();
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocumentViewer::ResumePainting()
+{
+  bool enablePaint = true;
+  CallChildren(ChangeChildPaintingEnabled, &enablePaint);
+
+  nsIPresShell* presShell = GetPresShell();
+  if (presShell) {
+    presShell->ResumePainting();
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocumentViewer::ChangeMaxLineBoxWidth(int32_t aMaxLineBoxWidth)
 {
   // Change the max line box width for all children.
   struct LineBoxInfo lbi = { aMaxLineBoxWidth };
   CallChildren(ChangeChildMaxLineBoxWidth, &lbi);
 
   // Now, change our max line box width.
   // Convert to app units, since our input is in CSS pixels.
   nscoord mlbw = nsPresContext::CSSPixelsToAppUnits(aMaxLineBoxWidth);
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -124,20 +124,20 @@ typedef struct CapturingContentInfo {
   bool mAllowed;
   bool mPointerLock;
   bool mRetargetToElement;
   bool mPreventDrag;
   nsIContent* mContent;
 } CapturingContentInfo;
 
 
-// f5b542a9-eaf0-4560-a656-37a9d379864c
+// 0e4f2b36-7ab8-43c5-b912-5c311566297c
 #define NS_IPRESSHELL_IID \
-{ 0xf5b542a9, 0xeaf0, 0x4560, \
-  { 0x37, 0xa9, 0xd3, 0x79, 0x86, 0x4c } }
+{ 0xde498c49, 0xf83f, 0x47bf, \
+  {0x8c, 0xc6, 0x8f, 0xf8, 0x74, 0x62, 0x22, 0x23 } }
 
 // debug VerifyReflow flags
 #define VERIFY_REFLOW_ON                    0x01
 #define VERIFY_REFLOW_NOISY                 0x02
 #define VERIFY_REFLOW_ALL                   0x04
 #define VERIFY_REFLOW_DUMP_COMMANDS         0x08
 #define VERIFY_REFLOW_NOISY_RC              0x10
 #define VERIFY_REFLOW_REALLY_NOISY_RC       0x20
@@ -832,16 +832,30 @@ public:
   /**
    * Called to find out if painting is suppressed for this presshell.  If it is suppressd,
    * we don't allow the painting of any layer but the background, and we don't
    * recur into our children.
    */
   bool IsPaintingSuppressed() const { return mPaintingSuppressed; }
 
   /**
+   * Pause painting by freezing the refresh driver of this and all parent
+   * presentations. This may not have the desired effect if this pres shell
+   * has its own refresh driver.
+   */
+  virtual void PausePainting() = 0;
+
+  /**
+   * Resume painting by thawing the refresh driver of this and all parent
+   * presentations. This may not have the desired effect if this pres shell
+   * has its own refresh driver.
+   */
+  virtual void ResumePainting() = 0;
+
+  /**
    * Unsuppress painting.
    */
   virtual NS_HIDDEN_(void) UnsuppressPainting() = 0;
 
   /**
    * Called to disable nsITheme support in a specific presshell.
    */
   void DisableThemeSupport()
@@ -1596,16 +1610,17 @@ protected:
   // Cached font inflation values. This is done to prevent changing of font
   // inflation until a page is reloaded.
   uint32_t mFontSizeInflationEmPerLine;
   uint32_t mFontSizeInflationMinTwips;
   uint32_t mFontSizeInflationLineThreshold;
   bool mFontSizeInflationForceEnabled;
   bool mFontSizeInflationDisabledInMasterProcess;
   bool mFontSizeInflationEnabled;
+  bool mPaintingIsFrozen;
 
   // Dirty bit indicating that mFontSizeInflationEnabled needs to be recomputed.
   bool mFontSizeInflationEnabledIsDirty;
 
   // Flag to indicate whether or not there is a reflow on zoom event pending.
   // See IsReflowOnZoomPending() for more information.
   bool mReflowOnZoomPending;
 
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -719,16 +719,18 @@ PresShell::PresShell()
   mMaxLineBoxWidth = 0;
 
   static bool addedSynthMouseMove = false;
   if (!addedSynthMouseMove) {
     Preferences::AddBoolVarCache(&sSynthMouseMove,
                                  "layout.reflow.synthMouseMove", true);
     addedSynthMouseMove = true;
   }
+
+  mPaintingIsFrozen = false;
 }
 
 NS_IMPL_ISUPPORTS7(PresShell, nsIPresShell, nsIDocumentObserver,
                    nsISelectionController,
                    nsISelectionDisplay, nsIObserver, nsISupportsWeakReference,
                    nsIMutationObserver)
 
 PresShell::~PresShell()
@@ -739,16 +741,23 @@ PresShell::~PresShell()
   }
 
   NS_ASSERTION(mCurrentEventContentStack.Count() == 0,
                "Huh, event content left on the stack in pres shell dtor!");
   NS_ASSERTION(mFirstCallbackEventRequest == nullptr &&
                mLastCallbackEventRequest == nullptr,
                "post-reflow queues not empty.  This means we're leaking");
 
+  // Verify that if painting was frozen, but we're being removed from the tree,
+  // that we now re-enable painting on our refresh driver, since it may need to
+  // be re-used by another presentation.
+  if (mPaintingIsFrozen) {
+    mPresContext->RefreshDriver()->Thaw();
+  }
+
 #ifdef DEBUG
   MOZ_ASSERT(mPresArenaAllocCount == 0,
              "Some pres arena objects were not freed");
 #endif
 
   delete mStyleSet;
   delete mFrameConstructor;
 
@@ -9926,8 +9935,28 @@ nsIPresShell::SetMaxLineBoxWidth(nscoord
   NS_ASSERTION(aMaxLineBoxWidth >= 0, "attempting to set max line box width to a negative value");
 
   if (mMaxLineBoxWidth != aMaxLineBoxWidth) {
     mMaxLineBoxWidth = aMaxLineBoxWidth;
     mReflowOnZoomPending = true;
     FrameNeedsReflow(GetRootFrame(), eResize, NS_FRAME_HAS_DIRTY_CHILDREN);
   }
 }
+
+void
+PresShell::PausePainting()
+{
+  if (GetPresContext()->RefreshDriver()->PresContext() != GetPresContext())
+    return;
+
+  mPaintingIsFrozen = true;
+  GetPresContext()->RefreshDriver()->Freeze();
+}
+
+void
+PresShell::ResumePainting()
+{
+  if (GetPresContext()->RefreshDriver()->PresContext() != GetPresContext())
+    return;
+
+  mPaintingIsFrozen = false;
+  GetPresContext()->RefreshDriver()->Thaw();
+}
--- a/layout/base/nsPresShell.h
+++ b/layout/base/nsPresShell.h
@@ -684,16 +684,19 @@ protected:
   bool ScheduleReflowOffTimer();
 
   // Widget notificiations
   virtual void WindowSizeMoveDone() MOZ_OVERRIDE;
   virtual void SysColorChanged() MOZ_OVERRIDE { mPresContext->SysColorChanged(); }
   virtual void ThemeChanged() MOZ_OVERRIDE { mPresContext->ThemeChanged(); }
   virtual void BackingScaleFactorChanged() MOZ_OVERRIDE { mPresContext->UIResolutionChanged(); }
 
+  virtual void PausePainting() MOZ_OVERRIDE;
+  virtual void ResumePainting() MOZ_OVERRIDE;
+
   void UpdateImageVisibility();
 
   nsRevocableEventPtr<nsRunnableMethod<PresShell> > mUpdateImageVisibilityEvent;
 
   void ClearVisibleImagesList();
   static void ClearImageVisibilityVisited(nsView* aView, bool aClear);
   static void MarkImagesInListVisible(const nsDisplayList& aList);
 
--- a/mobile/android/base/InputMethods.java
+++ b/mobile/android/base/InputMethods.java
@@ -61,16 +61,17 @@ final public class InputMethods {
 
     public static boolean shouldCommitCharAsKey(String inputMethod) {
         return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
     }
 
     @RobocopTarget
     public static boolean shouldDisableUrlBarUpdate(Context context) {
         String inputMethod = getCurrentInputMethod(context);
+        // HTC Touch Input does not react well to restarting during input (bug 909940)
         return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
     }
 
     public static boolean shouldDelayUrlBarUpdate(Context context) {
         String inputMethod = getCurrentInputMethod(context);
         return METHOD_SAMSUNG.equals(inputMethod) ||
                METHOD_SWIFTKEY.equals(inputMethod);
     }
--- a/mobile/android/base/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/gfx/JavaPanZoomController.java
@@ -240,41 +240,49 @@ class JavaPanZoomController
     public void handleMessage(String event, JSONObject message) {
         try {
             if (MESSAGE_ZOOM_RECT.equals(event)) {
                 float x = (float)message.getDouble("x");
                 float y = (float)message.getDouble("y");
                 final RectF zoomRect = new RectF(x, y,
                                      x + (float)message.getDouble("w"),
                                      y + (float)message.getDouble("h"));
-                mTarget.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        animatedZoomTo(zoomRect);
-                    }
-                });
+                if (message.optBoolean("animate", true)) {
+                    mTarget.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            animatedZoomTo(zoomRect);
+                        }
+                    });
+                } else {
+                    mTarget.setViewportMetrics(getMetricsToZoomTo(zoomRect));
+                }
             } else if (MESSAGE_ZOOM_PAGE.equals(event)) {
                 ImmutableViewportMetrics metrics = getMetrics();
                 RectF cssPageRect = metrics.getCssPageRect();
 
                 RectF viewableRect = metrics.getCssViewport();
                 float y = viewableRect.top;
                 // attempt to keep zoom keep focused on the center of the viewport
                 float newHeight = viewableRect.height() * cssPageRect.width() / viewableRect.width();
                 float dh = viewableRect.height() - newHeight; // increase in the height
                 final RectF r = new RectF(0.0f,
                                     y + dh/2,
                                     cssPageRect.width(),
                                     y + dh/2 + newHeight);
-                mTarget.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        animatedZoomTo(r);
-                    }
-                });
+                if (message.optBoolean("animate", true)) {
+                    mTarget.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            animatedZoomTo(r);
+                        }
+                    });
+                } else {
+                    mTarget.setViewportMetrics(getMetricsToZoomTo(r));
+                }
             } else if (MESSAGE_TOUCH_LISTENER.equals(event)) {
                 int tabId = message.getInt("tabID");
                 final Tab tab = Tabs.getInstance().getTab(tabId);
                 tab.setHasTouchListeners(true);
                 mTarget.post(new Runnable() {
                     @Override
                     public void run() {
                         if (Tabs.getInstance().isSelectedTab(tab))
@@ -1394,17 +1402,17 @@ class JavaPanZoomController
     }
 
     /**
      * Zoom to a specified rect IN CSS PIXELS.
      *
      * While we usually use device pixels, @zoomToRect must be specified in CSS
      * pixels.
      */
-    private boolean animatedZoomTo(RectF zoomToRect) {
+    private ImmutableViewportMetrics getMetricsToZoomTo(RectF zoomToRect) {
         final float startZoom = getMetrics().zoomFactor;
 
         RectF viewport = getMetrics().getViewport();
         // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
         // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
         // while enlarging make sure we enlarge equally on both sides to keep the target rect
         // centered.
         float targetRatio = viewport.width() / viewport.height();
@@ -1429,18 +1437,21 @@ class JavaPanZoomController
         finalMetrics = finalMetrics.setViewportOrigin(
             zoomToRect.left * finalMetrics.zoomFactor,
             zoomToRect.top * finalMetrics.zoomFactor);
         finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));
 
         // 2. now run getValidViewportMetrics on it, so that the target viewport is
         // clamped down to prevent overscroll, over-zoom, and other bad conditions.
         finalMetrics = getValidViewportMetrics(finalMetrics);
+        return finalMetrics;
+    }
 
-        bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM);
+    private boolean animatedZoomTo(RectF zoomToRect) {
+        bounce(getMetricsToZoomTo(zoomToRect), PanZoomState.ANIMATED_ZOOM);
         return true;
     }
 
     /** This function must be called from the UI thread. */
     @Override
     public void abortPanning() {
         checkMainThread();
         bounce();
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -68,16 +68,19 @@
 <!ENTITY pref_category_customize "Customize">
 <!ENTITY pref_category_search2 "Search settings">
 <!ENTITY pref_category_display "Display">
 <!ENTITY pref_category_privacy_short "Privacy">
 <!ENTITY pref_category_vendor "&vendorShortName;">
 <!ENTITY pref_category_datareporting "Data choices">
 <!ENTITY pref_category_installed_search_engines "Installed search engines">
 <!ENTITY pref_category_add_search_providers "Add more search providers">
+<!ENTITY pref_category_search_restore_defaults "Restore search engines">
+<!ENTITY pref_search_restore_defaults "Restore defaults">
+<!ENTITY pref_search_restore_defaults_summary "Restore defaults">
 <!-- Localization note (pref_search_tip) : "TIP" as in "hint", "clue" etc. Displayed as an
      advisory message on the customise search providers settings page explaining how to add new
      search providers.-->
 <!ENTITY pref_search_tip "TIP: Add any website to your list of search providers by long-pressing on its search field.">
 <!ENTITY pref_category_devtools "Developer tools">
 <!ENTITY pref_developer_remotedebugging "Remote debugging">
 <!ENTITY pref_developer_remotedebugging_docs "Learn more">
 <!ENTITY pref_remember_signons "Remember passwords">
--- a/mobile/android/base/menu/MenuItemActionBar.java
+++ b/mobile/android/base/menu/MenuItemActionBar.java
@@ -2,64 +2,127 @@
  * 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/. */
 
 package org.mozilla.gecko.menu;
 
 import org.mozilla.gecko.R;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
 
 public class MenuItemActionBar extends ImageButton
                                implements GeckoMenuItem.Layout {
     private static final String LOGTAG = "GeckoMenuItemActionBar";
 
+    private static Bitmap sMoreIcon;
+    private static float sHalfIconWidth;
+    private static float sMoreWidth;
+    private static int sMoreOffset;
+    private static Paint sDisabledPaint;
+
+    private Drawable mIcon;
+    private boolean mHasSubMenu = false;
+
     public MenuItemActionBar(Context context) {
         this(context, null);
     }
 
     public MenuItemActionBar(Context context, AttributeSet attrs) {
         this(context, attrs, R.attr.menuItemActionBarStyle);
     }
 
     public MenuItemActionBar(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+
+        if (sMoreIcon == null) {
+            final Resources res = getResources();
+
+            BitmapDrawable drawable  = (BitmapDrawable) res.getDrawable(R.drawable.menu_item_more);
+            sMoreIcon = drawable.getBitmap();
+
+            // The icon has some space on the right. Taking half the size feels better.
+            sMoreWidth = getResources().getDimensionPixelSize(R.dimen.menu_item_state_icon) / 2.0f;
+            sMoreOffset = res.getDimensionPixelSize(R.dimen.menu_item_more_offset);
+
+            final int rowHeight = res.getDimensionPixelSize(R.dimen.menu_item_row_height);
+            final int padding = getPaddingTop() + getPaddingBottom();
+            sHalfIconWidth = (rowHeight - padding) / 2.0f;
+
+            sDisabledPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+            sDisabledPaint.setColorFilter(new PorterDuffColorFilter(0xFF999999, PorterDuff.Mode.SRC_ATOP));
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (!mHasSubMenu) {
+            super.onDraw(canvas);
+            return;
+        }
+
+        final int count = canvas.save();
+
+        final float halfWidth = getMeasuredWidth() / 2.0f;
+        final float halfHeight = getMeasuredHeight() / 2.0f;
+
+        // If the width is small, the more icon might be pushed to the edges.
+        // Instead translate the canvas, so that both the icon + more is centered as a whole.
+        final boolean needsTranslation = (halfWidth < 1.5 * halfHeight);
+        final float translateX = needsTranslation ? (sMoreOffset + sMoreWidth) / 2.0f : 0.0f;
+
+        canvas.translate(-translateX, 0);
+
+        super.onDraw(canvas);
+
+        final float left = halfWidth + sHalfIconWidth + sMoreOffset - translateX;
+        final float top = halfHeight - sMoreWidth;
+
+        canvas.drawBitmap(sMoreIcon, left, top, isEnabled() ? null : sDisabledPaint);
+
+        canvas.translate(translateX, 0);
+
+        canvas.restoreToCount(count);
     }
 
     @Override
     public void initialize(GeckoMenuItem item) {
         if (item == null)
             return;
 
         setIcon(item.getIcon());
         setTitle(item.getTitle());
         setEnabled(item.isEnabled());
         setId(item.getItemId());
+        setSubMenuIndicator(item.hasSubMenu());
     }
 
     void setIcon(Drawable icon) {
-        if (icon != null) {
-            setImageDrawable(icon);
+        mIcon = icon;
+
+        if (icon == null) {
+            setVisibility(GONE);
+        } else {
             setVisibility(VISIBLE);
-        } else {
-            setVisibility(GONE);
+            setImageDrawable(icon);
         }
     }
 
     void setIcon(int icon) {
-        if (icon != 0) {
-            setImageResource(icon);
-            setVisibility(VISIBLE);
-        } else {
-            setVisibility(GONE);
-        }
+        setIcon((icon == 0) ? null : getResources().getDrawable(icon));
     }
 
     void setTitle(CharSequence title) {
         // set accessibility contentDescription here
         setContentDescription(title);
     }
 
     @Override
@@ -67,9 +130,16 @@ public class MenuItemActionBar extends I
         super.setEnabled(enabled);
         setColorFilter(enabled ? 0 : 0xFF999999);
     }
 
     @Override
     public void setShowIcon(boolean show) {
         // Do nothing.
     }
+
+    private void setSubMenuIndicator(boolean hasSubMenu) {
+        if (mHasSubMenu != hasSubMenu) {
+            mHasSubMenu = hasSubMenu;
+            invalidate();
+        }
+    }
 }
--- a/mobile/android/base/menu/MenuItemActionView.java
+++ b/mobile/android/base/menu/MenuItemActionView.java
@@ -63,36 +63,21 @@ public class MenuItemActionView extends 
         super.onLayout(changed, left, top, right, bottom);
     }
 
     @Override
     public void initialize(GeckoMenuItem item) {
         if (item == null)
             return;
 
-        setTitle(item.getTitle());
-        setIcon(item.getIcon());
+        mMenuItem.initialize(item);
+        mMenuButton.initialize(item);
         setEnabled(item.isEnabled());
     }
 
-    private void setIcon(Drawable icon) {
-        mMenuItem.setIcon(icon);
-        mMenuButton.setIcon(icon);
-    }
-
-    private void setIcon(int icon) {
-        mMenuItem.setIcon(icon);
-        mMenuButton.setIcon(icon);
-    }
-
-    private void setTitle(CharSequence title) {
-        mMenuItem.setTitle(title);
-        mMenuButton.setTitle(title);
-    }
-
     @Override
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
         mMenuItem.setEnabled(enabled);
         mMenuButton.setEnabled(enabled);
 
         for (ImageButton button : mActionButtons) {
              button.setEnabled(enabled);
--- a/mobile/android/base/menu/MenuItemDefault.java
+++ b/mobile/android/base/menu/MenuItemDefault.java
@@ -97,22 +97,17 @@ public class MenuItemDefault extends Tex
             mIcon.setBounds(sIconBounds);
             mIcon.setAlpha(isEnabled() ? 255 : 99);
         }
 
         refreshIcon();
     }
 
     void setIcon(int icon) {
-        Drawable drawable = null;
-
-        if (icon != 0)
-            drawable = getResources().getDrawable(icon);
-         
-        setIcon(drawable);
+        setIcon((icon == 0) ? null : getResources().getDrawable(icon));
     }
 
     void setTitle(CharSequence title) {
         setText(title);
     }
 
     @Override
     public void setEnabled(boolean enabled) {
--- a/mobile/android/base/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java
@@ -1,62 +1,118 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.preferences;
 
+import java.lang.reflect.Field;
+
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.PrefsHelper;
 
+import android.app.Activity;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.os.Bundle;
 import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.ViewConfiguration;
 
 /* A simple implementation of PreferenceFragment for large screen devices
  * This will strip category headers (so that they aren't shown to the user twice)
  * as well as initializing Gecko prefs when a fragment is shown.
 */
 public class GeckoPreferenceFragment extends PreferenceFragment {
 
     private static final String LOGTAG = "GeckoPreferenceFragment";
     private int mPrefsRequestId = 0;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        String resourceName = getArguments().getString("resource");
+
+        int res = getResource();
 
-        int res = 0;
-        if (resourceName != null) {
-            // Fetch resource id by resource name.
-            res = getActivity().getResources().getIdentifier(resourceName,
-                                                             "xml",
-                                                             getActivity().getPackageName());
+        // Display a menu for Search preferences.
+        if (res == R.xml.preferences_search) {
+            setHasOptionsMenu(true);
         }
 
-        if (res == 0) {
-            // The resource was invalid. Use the default resource.
-            Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings.");
-
-            boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane();
-            res = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences;
-        }
         addPreferencesFromResource(res);
 
         PreferenceScreen screen = getPreferenceScreen();
         setPreferenceScreen(screen);
         mPrefsRequestId = ((GeckoPreferences)getActivity()).setupPreferences(screen);
     }
 
+    /*
+     * Get the resource from Fragment arguments and return it.
+     *
+     * If no resource can be found, return the resource id of the default preference screen.
+     */
+    private int getResource() {
+        int resid = 0;
+
+        String resourceName = getArguments().getString("resource");
+        if (resourceName != null) {
+            // Fetch resource id by resource name.
+            resid = getActivity().getResources().getIdentifier(resourceName,
+                                                             "xml",
+                                                             getActivity().getPackageName());
+        }
+
+        if (resid == 0) {
+            // The resource was invalid. Use the default resource.
+            Log.e(LOGTAG, "Failed to find resource: " + resourceName + ". Displaying default settings.");
+
+            boolean isMultiPane = ((PreferenceActivity) getActivity()).onIsMultiPane();
+            resid = isMultiPane ? R.xml.preferences_customize_tablet : R.xml.preferences;
+        }
+
+        return resid;
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.preferences_search_menu, menu);
+    }
+
     @Override
     public void onDestroy() {
         super.onDestroy();
         if (mPrefsRequestId > 0) {
             PrefsHelper.removeObserver(mPrefsRequestId);
         }
     }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        showOverflowMenu(activity);
+    }
+
+    /*
+     * Force the overflow 3-dot menu to be displayed if it isn't already displayed.
+     *
+     * This is an ugly hack for 4.0+ Android devices that don't have a dedicated menu button
+     * because Android does not provide a public API to display the ActionBar overflow menu.
+     */
+    private void showOverflowMenu(Activity activity) {
+        try {
+            ViewConfiguration config = ViewConfiguration.get(activity);
+            Field menuOverflow = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
+            if (menuOverflow != null) {
+                menuOverflow.setAccessible(true);
+                menuOverflow.setBoolean(config, false);
+            }
+        } catch (Exception e) {
+            Log.d(LOGTAG, "Failed to force overflow menu, ignoring.");
+        }
+    }
 }
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -72,17 +72,19 @@ public class GeckoPreferences
     private static final String NON_PREF_PREFIX = "android.not_a_preference.";
     public static final String INTENT_EXTRA_RESOURCES = "resource";
     public static String PREFS_HEALTHREPORT_UPLOAD_ENABLED = NON_PREF_PREFIX + "healthreport.uploadEnabled";
 
     private static boolean sIsCharEncodingEnabled = false;
     private boolean mInitialized = false;
     private int mPrefsRequestId = 0;
 
-    // These match keys in resources/xml/preferences.xml.in.
+    // These match keys in resources/xml*/preferences*.xml
+    private static String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults";
+
     private static String PREFS_ANNOUNCEMENTS_ENABLED = NON_PREF_PREFIX + "privacy.announcements.enabled";
     private static String PREFS_DATA_REPORTING_PREFERENCES = NON_PREF_PREFIX + "datareporting.preferences";
     private static String PREFS_TELEMETRY_ENABLED = "datareporting.telemetry.enabled";
     private static String PREFS_CRASHREPORTER_ENABLED = "datareporting.crashreporter.submitEnabled";
     private static String PREFS_MENU_CHAR_ENCODING = "browser.menu.showCharacterEncoding";
     private static String PREFS_MP_ENABLED = "privacy.masterpassword.enabled";
     private static String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload";
     private static String PREFS_GEO_REPORTING = "app.geo.reportdata";
@@ -384,39 +386,64 @@ public class GeckoPreferences
                     CharSequence selectedEntry = listPref.getEntry();
                     listPref.setSummary(selectedEntry);
                     continue;
                 } else if (PREFS_SYNC.equals(key) && GeckoProfile.get(this).inGuestMode()) {
                     // Don't show sync prefs while in guest mode.
                     preferences.removePreference(pref);
                     i--;
                     continue;
+                } else if (PREFS_SEARCH_RESTORE_DEFAULTS.equals(key)) {
+                    pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+                        @Override
+                        public boolean onPreferenceClick(Preference preference) {
+                            GeckoPreferences.this.restoreDefaultSearchEngines();
+                            return true;
+                        }
+                    });
                 }
 
                 // Some Preference UI elements are not actually preferences,
                 // but they require a key to work correctly. For example,
                 // "Clear private data" requires a key for its state to be
                 // saved when the orientation changes. It uses the
                 // "android.not_a_preference.privacy.clear" key - which doesn't
                 // exist in Gecko - to satisfy this requirement.
                 if (key != null && !key.startsWith(NON_PREF_PREFIX)) {
                     prefs.add(key);
                 }
             }
         }
     }
 
+    /**
+     * Restore default search engines in Gecko and retrigger a search engine refresh.
+     */
+    protected void restoreDefaultSearchEngines() {
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:RestoreDefaults", null));
+
+        // Send message to Gecko to get engines. SearchPreferenceCategory listens for the response.
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
+    }
+
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
+        int itemId = item.getItemId();
+        switch (itemId) {
             case android.R.id.home:
                 finish();
                 return true;
         }
 
+        // Generated R.id.* apparently aren't constant expressions, so they can't be switched.
+        if (itemId == R.id.restore_defaults) {
+            restoreDefaultSearchEngines();
+            return true;
+       }
+
         return super.onOptionsItemSelected(item);
     }
 
     final private int DIALOG_CREATE_MASTER_PASSWORD = 0;
     final private int DIALOG_REMOVE_MASTER_PASSWORD = 1;
 
     public static void setCharEncodingState(boolean enabled) {
         sIsCharEncodingEnabled = enabled;
--- a/mobile/android/base/preferences/SearchEnginePreference.java
+++ b/mobile/android/base/preferences/SearchEnginePreference.java
@@ -35,18 +35,16 @@ public class SearchEnginePreference exte
     public static final int INDEX_SET_DEFAULT_BUTTON = 0;
     public static final int INDEX_REMOVE_BUTTON = 1;
 
     // Cache label to avoid repeated use of the resource system.
     public final String LABEL_IS_DEFAULT;
 
     // Specifies if this engine is configured as the default search engine.
     private boolean mIsDefaultEngine;
-    // Specifies if this engine is one of the ones bundled with the app, which cannot be deleted.
-    private boolean mIsImmutableEngine;
 
     // Dialog element labels.
     private String[] mDialogItems;
 
     // The popup displayed when this element is tapped.
     private AlertDialog mDialog;
 
     private final SearchPreferenceCategory mParentCategory;
@@ -116,22 +114,17 @@ public class SearchEnginePreference exte
     /**
      * Configure this Preference object from the Gecko search engine JSON object.
      * @param geckoEngineJSON The Gecko-formatted JSON object representing the search engine.
      * @throws JSONException If the JSONObject is invalid.
      */
     public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException {
         final String engineName = geckoEngineJSON.getString("name");
         final SpannableString titleSpannable = new SpannableString(engineName);
-        mIsImmutableEngine = geckoEngineJSON.getBoolean("immutable");
 
-        if (mIsImmutableEngine) {
-            // Delete the "Remove" option from the menu.
-            mDialogItems = new String[] { getContext().getResources().getString(R.string.pref_search_set_default) };
-        }
         setTitle(titleSpannable);
 
         final String iconURI = geckoEngineJSON.getString("iconURI");
         // Keep a reference to the bitmap - we'll need it later in onBindView.
         try {
             mIconBitmap = BitmapUtils.getBitmapFromDataURI(iconURI);
         } catch (IllegalArgumentException e) {
             Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e);
@@ -171,21 +164,16 @@ public class SearchEnginePreference exte
                 @Override
                 public void run() {
                     Toast.makeText(getContext(), R.string.pref_search_last_toast, Toast.LENGTH_SHORT).show();
                 }
             });
             return;
         }
 
-        // If we are both default and immutable, we have no enabled items to show on the menu - abort.
-        if (mIsDefaultEngine && mIsImmutableEngine) {
-            return;
-        }
-
         final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
         builder.setTitle(getTitle().toString());
         builder.setItems(mDialogItems, new DialogInterface.OnClickListener() {
             // Forward the various events that we care about to the container class for handling.
             @Override
             public void onClick(DialogInterface dialog, int indexClicked) {
                 hideDialog();
                 switch (indexClicked) {
--- a/mobile/android/base/preferences/SearchPreferenceCategory.java
+++ b/mobile/android/base/preferences/SearchPreferenceCategory.java
@@ -38,37 +38,41 @@ public class SearchPreferenceCategory ex
 
     @Override
     protected void onAttachedToActivity() {
         super.onAttachedToActivity();
 
         // Ensures default engine remains at top of list.
         setOrderingAsAdded(true);
 
-        // Request list of search engines from Gecko.
+        // Register for SearchEngines messages and request list of search engines from Gecko.
         GeckoAppShell.registerEventListener("SearchEngines:Data", this);
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null));
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
+    }
+
+    @Override
+    protected void onPrepareForRemoval() {
+        GeckoAppShell.unregisterEventListener("SearchEngines:Data", this);
     }
 
     @Override
     public void handleMessage(String event, final JSONObject data) {
         if (event.equals("SearchEngines:Data")) {
-            // We are no longer interested in this event from Gecko, as we do not request it again with
-            // this instance.
-            GeckoAppShell.unregisterEventListener("SearchEngines:Data", this);
-
             // Parse engines array from JSON.
             JSONArray engines;
             try {
                 engines = data.getJSONArray("searchEngines");
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Unable to decode search engine data from Gecko.", e);
                 return;
             }
 
+            // Clear the preferences category from this thread.
+            this.removeAll();
+
             // Create an element in this PreferenceCategory for each engine.
             for (int i = 0; i < engines.length(); i++) {
                 try {
                     JSONObject engineJSON = engines.getJSONObject(i);
                     final String engineName = engineJSON.getString("name");
 
                     SearchEnginePreference enginePreference = new SearchEnginePreference(getContext(), this);
                     enginePreference.setSearchEngineFromJSON(engineJSON);
index 7f344814e0ae7633f575a1a6eb20170c23b95e01..01629dd15ebf9cb38f62b3c172b98c455a0024db
GIT binary patch
literal 1117
zc$}S6OK8+U7+z79S}Yz!P>V38Jy>iuGuhqdVOL3$-FCsPbzQZ15YuFK8`?ZfrtY=}
z(e_|HR7IrVMFg+)h2A{)Lb1?`pn@VIR4-Cmsp3HoI%&IAr1aoGGV@Qq@BhF2k8@p}
z?RB-=Y9o<IUA9BZ)4e`?YNGT#_VU{yx@{%tZgS8qlCtjOh+?`u7-Vg|g!5Q82Tx4m
z)<|S+pH=84-MM|T;o31h#KbDL7YSGE-ioIieVBkAT(X=LGk5<111vMe97*P2&Qo!(
z)iLDbgF~GKW2n!NOlI#M&{~nH0~-?^RP25ykSi%>!LLlu;WfvA1qkU&F^i(Qb6r4n
zeGHPZBx^vF0D=@lNt6&#GvFbLb1=@KIE#20N-|1<rNz)_zFCy>T4pI0wNgwkA)d@}
z<#IVzj>lZT#34zNILLE6&r$>%3_3)wuujlW^`POv@GXy6t^-0_?{No6ieak1V0*dT
zvSTM$N)$~QSJ6EV#UN+fTooOVJT9(|4hn-F=JGgj2YiFpquB6QsMV<<kgAdQ`xY&V
z-mkgFfQ=oJ)lv-o5;HAR&WJ(=At3<;9=3=GiBODZ+IUHk)HKh>QPsyv+!jS>lM)%F
z1t}^*>A1otgfy@4Y7%N`O^sK%StlU6W8f;+`ooo0<;tp$b>jL3*X^%%Kv%Cz+@RO>
zfU1I>Io+_F&~6Irvm_P!)=6w;eAfmG36`xD1PG#zMVNrBAPJC7U{PRoUhH87Skzm1
zu_zfuDq@ju{!ijKDv0~1W2#rQ4a4Jd|5Iamg0Mr!#;5Z!zh(P9I$NVzO(|5~&b&TQ
z`w-MbKh$1$c}2cln(uTr*3UF0eoaQ#zfd2y4<A1C>eA*LjnAF|<?fUBKSkl{r_q|3
z;`GrbY&Ld8CtBgDO(VnR)X3?N&+A4$PHjAMuWJl{`8xf1cK7%3J6Ji^yl(I|G@pWt
urO#(~?24Y5*uUZWkBQk^@8)i+r`JTTJ!*V2-hBQ@IPlqYr*^aL`0#IgByFnz
index 1d41cc5803d146613ca3c5f6aa76fcd1232891dd..46db43f439f0bbd6f51996467fcfa6542e9d77ab
GIT binary patch
literal 1075
zc$}S6O>5LZ7!FiKN~>TGwt^T^5rrn1$;ak{u4S9-b{E{bY!|x+l}?h`ZrII-$<*EU
zC|J;nC-quT^jOfV2Nm?-Merbk7r~<!|A0^@ZMTY)9vn#KeJ9WJ&Xf1!Zf$;Ubolfz
z!!V<jvR$Y9$?Q3Hl)kTie6UEj2~up5g{Vb(D8Wp@iyX{W0<?_l7<sEVKj3MG8S40r
zCTUhLn{E^cD8mT-AZD^PJ=2eo+rfl&@UkD~xu36IbFA;>xka@Ks<DOJet9jy3v2TY
zcdg?Z9yfD|o$i}7K!6Ei`$0EM&3>NSiEC0jI~F;12SPe|ZdX*ZT4Sv!!K^B%ybGYh
zYK8z+sOWHtl>n4QAd67up=1KXgetqYIC@*+wamI*+PjN-d9F=JY>Hy9*AsfO5GBhZ
zGz>!ok|;?$MeylrNKl^-)5$>u8>epK$Hb39HuI4at&lv&4SpertJVF$VY-(nnliDE
zVi5{J3<7b0PDvfN4o9br)fkI)oJK2&OY6~^{OhX2sTq)}F|Q;(Eeh(|k-HM$kW}nE
zN52T3@0lfCD?z9!K$AdDhfoK)T$+`Pf?*XUNrr<M2XS)+t*De_H3th8ghja^DOyn~
zNR|rhqHW0oZY4|!3SB(l`hU2}p<L5SFd|XXh@$SG18VJvL}@#US<7NCRFUh4nSU{>
z&z@AA__wfEN}_<>NwDc3AVAX*)`0?e&Cmd^U|r*pq&vI@S|}&!EyHb55xacve-bBB
zLE=9hGdQAcn3?<iPo4b<!XX`-gwDsE>rc1nY~85Xg+~9Y^J{y1W=Lhmh8{7WN1V6k
z+m7>cy!`&j#-@?3Po3hQuYYkW>d59sS-M|a8VknZi_$|xs&gBku9Y4a?1|Z~dGFb~
j&biFshQaY~&Jp$vb7phwZ1=>E@7Wkviu3l??9%Pu-7!uC
index 129ac1ed5bb64f0faf1cec2f30f496fa7873a08f..22f57d347675838dab10076e60e3952c49f10206
GIT binary patch
literal 1146
zc$}S7PiPcZ7@uM&DhWtcFhOA)(~_dI@6YV+{4rado!K?pcGGpU#841@nR%NXHuJ}s
zm+mA74=F+|2=&lvp$9KLl>YV5UP@69h3Y{FUP?uw&_fR`9)y5`*f%komLfU$VCH?_
zd%xfJee?a9^Nq=4L)%BT3xY6Io3NXFAI+YvFZ1{B!sW|+8)lUmHWjv6kHl0k-EfwQ
zHJ{AUCME9TnVWQ65Vmx^)(o4ezil|7pCcJ2m->;At??siM4T>V;w+u>f|B&$$7_=4
zxh3gDp$_YjMLXWaQcS0oCR@%@*U??+$Q$B#YH$NSWkgK<`5-aUlC);m;Bs~hBykPG
zx+UpZP&4(0XoWEq3%P>qK$I6%J%<V?uc1RChDZTW0Z5S%HlS{xg1EjU?k#rPM$@jY
z`{G(j>M#}=0Q7pjTu;e`@f<+9t^<ex#xh69$zs4rDhJ7dz6F~mPV7a@3j;CJ$!xg5
zN|Mz71V5_RHw*{KdZKvBKuRKjauE1F=%W+Xr0vboNoz5pph=T(A$E8@+6VsQYIAA^
z<k1-K#2zmSnYTk{!KVSM*(HhJ<Xq1+s+w9wNX<hP!=i?e1~sKx#=5Cn6^s?sxA8n~
z(NxQNzFH|F(?Y1Cm^iOiu!*e#v@5oy^trVlVI**9pX>e0wKwG&R!j*C<5n2X_dB4`
z30atQ!br3%ajZ@pFUa)Qv-+%uN@MRm>Q>{>7uOPOc+Uwy)d<yK9?Gh&LOD-0RVG-Q
zl~vd#MXa@Tr_F<S#&=&NaexN|{?{@6Bi@FYxY7Sy*_a?2@Ue;ceC&VtZOUis^O|k8
z(%=62_3F@dad6=12ly2Q?NYAptX-5|U443N`j1P}$cH<}eyE)6emtSS`Q7N4*|~l1
z!q?xQ1y1kjXH#d(Pac&|e!7>wbOAJ`KiW%w!uqX;ci%dG-x#&_pF6%>K3LfIb7x;`
z*O!-88rS|_eK0lr$!lK;D{yef?)R7f+%qtGBQ^1$ab*Slvi0DN^k|dI=SB$q;?d)a
dR|d`}+n{iG7+LQY@0`m12DQqh{Z0Aw@;~f0Zk7N5
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@+id/restore_defaults"
+          android:title="@string/pref_search_restore_defaults" />
+
+</menu>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -44,16 +44,17 @@
     <dimen name="doorhanger_padding">15dp</dimen>
     <dimen name="doorhanger_textsize_small">8sp</dimen>
 
     <dimen name="flow_layout_spacing">6dp</dimen>
     <dimen name="menu_item_icon">21dp</dimen>
     <dimen name="menu_item_state_icon">18dp</dimen>
     <dimen name="menu_item_row_height">44dp</dimen>
     <dimen name="menu_item_row_width">240dp</dimen>
+    <dimen name="menu_item_more_offset">5dp</dimen>
     <dimen name="menu_popup_arrow_margin">5dip</dimen>
     <dimen name="menu_popup_arrow_width">40dip</dimen>
     <dimen name="menu_popup_offset">8dp</dimen>
     <dimen name="menu_popup_width">256dp</dimen>
     <dimen name="nav_button_border_width">0.75dp</dimen>
     <dimen name="prompt_service_group_padding_size">32dp</dimen>
     <dimen name="prompt_service_icon_size">72dp</dimen>
     <dimen name="prompt_service_icon_text_padding">10dp</dimen>
copy from mobile/android/base/resources/xml/preferences_search.xml
copy to mobile/android/base/resources/xml-v11/preferences_search.xml
--- a/mobile/android/base/resources/xml/preferences_search.xml
+++ b/mobile/android/base/resources/xml/preferences_search.xml
@@ -11,16 +11,23 @@
     <CheckBoxPreference android:key="browser.search.suggest.enabled"
                         android:title="@string/pref_search_suggestions"
                         android:defaultValue="false"
                         android:persistent="false" />
 
     <org.mozilla.gecko.preferences.SearchPreferenceCategory
                         android:title="@string/pref_category_installed_search_engines"/>
 
+    <PreferenceCategory android:title="@string/pref_category_search_restore_defaults">
+
+        <Preference android:key="android.not_a_preference.search.restore_defaults"
+                    android:title="@string/pref_search_restore_defaults_summary" />
+
+    </PreferenceCategory>
+
     <PreferenceCategory android:title="@string/pref_category_add_search_providers">
 
         <Preference android:layout="@layout/preference_search_tip"
                     android:enabled="false"
                     android:selectable="false"/>
 
     </PreferenceCategory>
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -88,17 +88,21 @@
   <string name="pref_category_customize">&pref_category_customize;</string>
   <string name="pref_category_search">&pref_category_search2;</string>
   <string name="pref_category_display">&pref_category_display;</string>
   <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
   <string name="pref_category_vendor">&pref_category_vendor;</string>
   <string name="pref_category_datareporting">&pref_category_datareporting;</string>
   <string name="pref_category_installed_search_engines">&pref_category_installed_search_engines;</string>
   <string name="pref_category_add_search_providers">&pref_category_add_search_providers;</string>
+  <string name="pref_category_search_restore_defaults">&pref_category_search_restore_defaults;</string>
+  <string name="pref_search_restore_defaults">&pref_search_restore_defaults;</string>
+  <string name="pref_search_restore_defaults_summary">&pref_search_restore_defaults_summary;</string>
   <string name="pref_search_tip">&pref_search_tip;</string>
+
   <string name="pref_category_devtools">&pref_category_devtools;</string>
   <string name="pref_developer_remotedebugging">&pref_developer_remotedebugging;</string>
   <string name="pref_developer_remotedebugging_docs">&pref_developer_remotedebugging_docs;</string>
 
   <string name="pref_header_customize">&pref_header_customize;</string>
   <string name="pref_header_display">&pref_header_display;</string>
   <string name="pref_header_privacy_short">&pref_header_privacy_short;</string>
   <string name="pref_header_vendor">&pref_header_vendor;</string>
--- a/mobile/android/base/tests/testDistribution.java
+++ b/mobile/android/base/tests/testDistribution.java
@@ -154,17 +154,17 @@ public class testDistribution extends Co
 
         } catch (JSONException e) {
             mAsserter.ok(false, "exception getting preferences", e.toString());
         }
     }
 
     private void checkSearchPlugin() {
         Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data");
-        mActions.sendGeckoEvent("SearchEngines:Get", null);
+        mActions.sendGeckoEvent("SearchEngines:GetVisible", null);
 
         try {
             JSONObject data = new JSONObject(eventExpecter.blockForEventData());
             eventExpecter.unregisterListener();
             JSONArray searchEngines = data.getJSONArray("searchEngines");
             boolean foundEngine = false;
             for (int i = 0; i < searchEngines.length(); i++) {
                 JSONObject engine = (JSONObject) searchEngines.get(i);
--- a/mobile/android/base/toolbar/ToolbarEditText.java
+++ b/mobile/android/base/toolbar/ToolbarEditText.java
@@ -42,17 +42,20 @@ public class ToolbarEditText extends Cus
     }
 
     interface OnTextTypeChangeListener {
         public void onTextTypeChange(ToolbarEditText editText, TextType textType);
     }
 
     private final Context mContext;
 
-    private TextType mTextType;
+    // Type of the URL bar go/search button
+    private TextType mToolbarTextType;
+    // Type of the keyboard go/search button (cannot be EMPTY)
+    private TextType mKeyboardTextType;
 
     private OnCommitListener mCommitListener;
     private OnDismissListener mDismissListener;
     private OnFilterListener mFilterListener;
     private OnTextTypeChangeListener mTextTypeListener;
 
     // The previous autocomplete result returned to us
     private String mAutoCompleteResult = "";
@@ -61,17 +64,18 @@ public class ToolbarEditText extends Cus
     private String mAutoCompletePrefix = null;
 
     private boolean mDelayRestartInput;
 
     public ToolbarEditText(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
 
-        mTextType = TextType.EMPTY;
+        mToolbarTextType = TextType.EMPTY;
+        mKeyboardTextType = TextType.URL;
     }
 
     void setOnCommitListener(OnCommitListener listener) {
         mCommitListener = listener;
     }
 
     void setOnDismissListener(OnDismissListener listener) {
         mDismissListener = listener;
@@ -167,30 +171,35 @@ public class ToolbarEditText extends Cus
                 }
             }
         }
 
         return false;
     }
 
     private void setTextType(TextType textType) {
-        mTextType = textType;
+        mToolbarTextType = textType;
 
+        if (textType != TextType.EMPTY) {
+            mKeyboardTextType = textType;
+        }
         if (mTextTypeListener != null) {
-            mTextTypeListener.onTextTypeChange(this, mTextType);
+            mTextTypeListener.onTextTypeChange(this, textType);
         }
     }
 
     private void updateTextTypeFromText(String text) {
         if (text.length() == 0) {
             setTextType(TextType.EMPTY);
             return;
         }
 
         if (InputMethods.shouldDisableUrlBarUpdate(mContext)) {
+            // Set button type to match the previous keyboard type
+            setTextType(mKeyboardTextType);
             return;
         }
 
         final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION;
 
         final int imeAction;
         if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) {
             imeAction = EditorInfo.IME_ACTION_SEARCH;
@@ -217,20 +226,26 @@ public class ToolbarEditText extends Cus
         } else if (mDelayRestartInput) {
             // Only call delayed restartInput when actionBits == imeAction
             // so if there are two restarts in a row, the first restarts will
             // be discarded and the second restart will be properly delayed
             mDelayRestartInput = false;
             restartInput = true;
         }
 
-        if (restartInput) {
-            updateKeyboardInputType();
-            imm.restartInput(ToolbarEditText.this);
+        if (!restartInput) {
+            // If the text content was previously empty, the toolbar text type
+            // is empty as well. Since the keyboard text type cannot be empty,
+            // the two text types are now inconsistent. Reset the toolbar text
+            // type here to the keyboard text type to ensure consistency.
+            setTextType(mKeyboardTextType);
+            return;
         }
+        updateKeyboardInputType();
+        imm.restartInput(ToolbarEditText.this);
 
         setTextType(imeAction == EditorInfo.IME_ACTION_GO ?
                     TextType.URL : TextType.SEARCH_QUERY);
     }
 
     private class TextChangeListener implements TextWatcher {
         @Override
         public void afterTextChanged(final Editable s) {
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -173,20 +173,30 @@ function doChangeMaxLineBoxWidth(aWidth)
   let docShell = webNav.QueryInterface(Ci.nsIDocShell);
   let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
 
   let range = null;
   if (BrowserApp.selectedTab._mReflozPoint) {
     range = BrowserApp.selectedTab._mReflozPoint.range;
   }
 
-  docViewer.changeMaxLineBoxWidth(aWidth);
-
-  if (range) {
-    BrowserEventHandler._zoomInAndSnapToRange(range);
+  try {
+    docViewer.pausePainting();
+    docViewer.changeMaxLineBoxWidth(aWidth);
+
+    if (range) {
+      BrowserEventHandler._zoomInAndSnapToRange(range);
+    } else {
+      // In this case, we actually didn't zoom into a specific range. It
+      // probably happened from a page load reflow-on-zoom event, so we
+      // need to make sure painting is re-enabled.
+      BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
+    }
+  } finally {
+    docViewer.resumePainting();
   }
 }
 
 function fuzzyEquals(a, b) {
   return (Math.abs(a - b) < 1e-6);
 }
 
 /**
@@ -2739,16 +2749,17 @@ Tab.prototype = {
     this.browser.addEventListener("scroll", this, true);
     this.browser.addEventListener("MozScrolledAreaChanged", this, true);
     // Note that the XBL binding is untrusted
     this.browser.addEventListener("PluginBindingAttached", this, true, true);
     this.browser.addEventListener("pageshow", this, true);
     this.browser.addEventListener("MozApplicationManifest", this, true);
 
     Services.obs.addObserver(this, "before-first-paint", false);
+    Services.obs.addObserver(this, "after-viewport-change", false);
     Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
 
     if (aParams.delayLoad) {
       // If this is a zombie tab, attach restore data so the tab will be
       // restored when selected
       this.browser.__SS_data = {
         entries: [{
           url: aURL,
@@ -2798,31 +2809,68 @@ Tab.prototype = {
     // We only use the font.size.inflation.minTwips preference because this is
     // the only one that is controlled by the user-interface in the 'Settings'
     // menu. Thus, if font.size.inflation.emPerLine is changed, this does not
     // effect reflow-on-zoom.
     let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips"));
     return minFontSize / this.getInflatedFontSizeFor(aElement);
   },
 
+  clearReflowOnZoomPendingActions: function() {
+    // Reflow was completed, so now re-enable painting.
+    let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+    let docShell = webNav.QueryInterface(Ci.nsIDocShell);
+    let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
+    docViewer.resumePainting();
+
+    BrowserApp.selectedTab._mReflozPositioned = false;
+  },
+
+  /**
+   * Reflow on zoom consists of a few different sub-operations:
+   *
+   * 1. When a double-tap event is seen, we verify that the correct preferences
+   *    are enabled and perform the pre-position handling calculation. We also
+   *    signal that reflow-on-zoom should be performed at this time, and pause
+   *    painting.
+   * 2. During the next call to setViewport(), which is in the Tab prototype,
+   *    we detect that a call to changeMaxLineBoxWidth should be performed. If
+   *    we're zooming out, then the max line box width should be reset at this
+   *    time. Otherwise, we call performReflowOnZoom.
+   *   2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
+   *       doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
+   * 3. doChangeMaxLineBoxWidth changes the line box width (which also
+   *    schedules a reflow event), and then calls _zoomInAndSnapToRange.
+   * 4. _zoomInAndSnapToRange performs the positioning of reflow-on-zoom and
+   *    then re-enables painting.
+   *
+   * Some of the events happen synchronously, while others happen asynchronously.
+   * The following is a rough sketch of the progression of events:
+   *
+   * double tap event seen -> onDoubleTap() -> ... asynchronous ...
+   *   -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
+   *   -> doChangeMaxLineBoxWidth() -> _zoomInAndSnapToRange()
+   *   -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
+   *   -> resumePainting()
+   */
   performReflowOnZoom: function(aViewport) {
-      let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
-
-      let viewportWidth = gScreenWidth / zoom;
-      let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
-
-      if (gReflowPending) {
-        clearTimeout(gReflowPending);
-      }
-
-      // We add in a bit of fudge just so that the end characters
-      // don't accidentally get clipped. 15px is an arbitrary choice.
-      gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
-                                  reflozTimeout,
-                                  viewportWidth - 15);
+    let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
+
+    let viewportWidth = gScreenWidth / zoom;
+    let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
+
+    if (gReflowPending) {
+      clearTimeout(gReflowPending);
+    }
+
+    // We add in a bit of fudge just so that the end characters
+    // don't accidentally get clipped. 15px is an arbitrary choice.
+    gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
+                                reflozTimeout,
+                                viewportWidth - 15);
   },
 
   /** 
    * Reloads the tab with the desktop mode setting.
    */
   reloadWithMode: function (aDesktopMode) {
     // Set desktop mode for tab and send change to Java
     if (this.desktopMode != aDesktopMode) {
@@ -2884,16 +2932,17 @@ Tab.prototype = {
     this.browser.removeEventListener("blur", this, true);
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
     this.browser.removeEventListener("PluginBindingAttached", this, true);
     this.browser.removeEventListener("pageshow", this, true);
     this.browser.removeEventListener("MozApplicationManifest", this, true);
 
     Services.obs.removeObserver(this, "before-first-paint");
+    Services.obs.removeObserver(this, "after-viewport-change");
     Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
 
     // Make sure the previously selected panel remains selected. The selected panel of a deck is
     // not stable when panels are removed.
     let selectedPanel = BrowserApp.deck.selectedPanel;
     BrowserApp.deck.removeChild(this.browser);
     BrowserApp.deck.selectedPanel = selectedPanel;
 
@@ -3170,23 +3219,31 @@ Tab.prototype = {
       // because we are pinch-zooming to zoom out.
       BrowserEventHandler.resetMaxLineBoxWidth();
       BrowserApp.selectedTab.reflozPinchSeen = false;
     } else if (BrowserApp.selectedTab.reflozPinchSeen &&
                isZooming) {
       // In this case, the user pinch-zoomed in, so we don't want to
       // preserve position as we would with reflow-on-zoom.
       BrowserApp.selectedTab.probablyNeedRefloz = false;
+      BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
       BrowserApp.selectedTab._mReflozPoint = null;
     }
 
+    let docViewer = null;
+
     if (isZooming &&
         BrowserEventHandler.mReflozPref &&
         BrowserApp.selectedTab._mReflozPoint &&
         BrowserApp.selectedTab.probablyNeedRefloz) {
+      let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+      let docShell = webNav.QueryInterface(Ci.nsIDocShell);
+      docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
+      docViewer.pausePainting();
+
       BrowserApp.selectedTab.performReflowOnZoom(aViewport);
       BrowserApp.selectedTab.probablyNeedRefloz = false;
     }
 
     let win = this.browser.contentWindow;
     win.scrollTo(x, y);
 
     this.userScrollPos.x = win.scrollX;
@@ -3205,16 +3262,19 @@ Tab.prototype = {
     let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
     dwi.setContentDocumentFixedPositionMargins(
       aViewport.fixedMarginTop / aViewport.zoom,
       aViewport.fixedMarginRight / aViewport.zoom,
       aViewport.fixedMarginBottom / aViewport.zoom,
       aViewport.fixedMarginLeft / aViewport.zoom);
 
     Services.obs.notifyObservers(null, "after-viewport-change", "");
+    if (docViewer) {
+        docViewer.resumePainting();
+    }
   },
 
   setResolution: function(aZoom, aForce) {
     // Set zoom level
     if (aForce || !fuzzyEquals(aZoom, this._zoom)) {
       this._zoom = aZoom;
       if (BrowserApp.selectedTab == this) {
         let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
@@ -4161,16 +4221,21 @@ Tab.prototype = {
 
         if (rzEnabled && rzPl) {
           // Retrieve the viewport width and adjust the max line box width
           // accordingly.
           let vp = BrowserApp.selectedTab.getViewport();
           BrowserApp.selectedTab.performReflowOnZoom(vp);
         }
         break;
+      case "after-viewport-change":
+        if (BrowserApp.selectedTab._mReflozPositioned) {
+          BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
+        }
+        break;
       case "nsPref:changed":
         if (aData == "browser.ui.zoom.force-user-scalable")
           ViewportHandler.updateMetadata(this, false);
         break;
     }
   },
 
   set readerEnabled(isReaderEnabled) {
@@ -4479,23 +4544,33 @@ var BrowserEventHandler = {
     let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
 
     // We only want to do this if reflow-on-zoom is enabled, we don't already
     // have a reflow-on-zoom event pending, and the element upon which the user
     // double-tapped isn't of a type we want to avoid reflow-on-zoom.
     if (BrowserEventHandler.mReflozPref &&
        !BrowserApp.selectedTab._mReflozPoint &&
        !this._shouldSuppressReflowOnZoom(element)) {
-     let data = JSON.parse(aData);
-     let zoomPointX = data.x;
-     let zoomPointY = data.y;
-
-     BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
-       range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
-       BrowserApp.selectedTab.probablyNeedRefloz = true;
+
+      // See comment above performReflowOnZoom() for a detailed description of
+      // the events happening in the reflow-on-zoom operation.
+      let data = JSON.parse(aData);
+      let zoomPointX = data.x;
+      let zoomPointY = data.y;
+
+      BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
+        range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
+
+      // Before we perform a reflow on zoom, let's disable painting.
+      let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+      let docShell = webNav.QueryInterface(Ci.nsIDocShell);
+      let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
+      docViewer.pausePainting();
+
+      BrowserApp.selectedTab.probablyNeedRefloz = true;
     }
 
     if (!element) {
       this._zoomOut();
       return;
     }
 
     while (element && !this._shouldZoomToElement(element))
@@ -4585,21 +4660,17 @@ var BrowserEventHandler = {
     if (rect.w > viewport.cssWidth || rect.h > viewport.cssHeight) {
       BrowserEventHandler.resetMaxLineBoxWidth();
     }
 
     sendMessageToJava(rect);
   },
 
   _zoomInAndSnapToRange: function(aRange) {
-    if (!aRange) {
-      Cu.reportError("aRange is null in zoomInAndSnapToRange. Unable to maintain position.");
-      return;
-    }
-
+    // aRange is always non-null here, since a check happened previously.
     let viewport = BrowserApp.selectedTab.getViewport();
     let fudge = 15; // Add a bit of fudge.
     let boundingElement = aRange.offsetNode;
     while (!boundingElement.getBoundingClientRect && boundingElement.parentNode) {
       boundingElement = boundingElement.parentNode;
     }
 
     let rect = ElementTouchHelper.getBoundingContentRect(boundingElement);
@@ -4612,40 +4683,43 @@ var BrowserEventHandler = {
     // center the area of interest on the screen.
     let topPos = scrollTop + drRect.top - (viewport.cssHeight / 2.0);
 
     // Factor in the border and padding
     let boundingStyle = window.getComputedStyle(boundingElement);
     let leftAdjustment = parseInt(boundingStyle.paddingLeft) +
                          parseInt(boundingStyle.borderLeftWidth);
 
+    BrowserApp.selectedTab._mReflozPositioned = true;
+
     rect.type = "Browser:ZoomToRect";
     rect.x = Math.max(viewport.cssPageLeft, rect.x  - fudge + leftAdjustment);
     rect.y = Math.max(topPos, viewport.cssPageTop);
     rect.w = viewport.cssWidth;
     rect.h = viewport.cssHeight;
+    rect.animate = false;
 
     sendMessageToJava(rect);
     BrowserApp.selectedTab._mReflozPoint = null;
-   },
-
-   onPinchFinish: function(aData) {
-     let data = {};
-     try {
-       data = JSON.parse(aData);
-     } catch(ex) {
-       console.log(ex);
-       return;
-     }
-
-     if (BrowserEventHandler.mReflozPref &&
-         data.zoomDelta < 0.0) {
-       BrowserEventHandler.resetMaxLineBoxWidth();
-     }
-   },
+  },
+
+  onPinchFinish: function(aData) {
+    let data = {};
+    try {
+      data = JSON.parse(aData);
+    } catch(ex) {
+      console.log(ex);
+      return;
+    }
+
+    if (BrowserEventHandler.mReflozPref &&
+        data.zoomDelta < 0.0) {
+      BrowserEventHandler.resetMaxLineBoxWidth();
+    }
+  },
 
   _shouldZoomToElement: function(aElement) {
     let win = aElement.ownerDocument.defaultView;
     if (win.getComputedStyle(aElement, null).display == "inline")
       return false;
     if (aElement instanceof Ci.nsIDOMHTMLLIElement)
       return false;
     if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
@@ -6603,20 +6677,20 @@ OverscrollController.prototype = {
 
 var SearchEngines = {
   _contextMenuId: null,
   PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
   PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
 
   init: function init() {
     Services.obs.addObserver(this, "SearchEngines:Add", false);
-    Services.obs.addObserver(this, "SearchEngines:Get", false);
     Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
+    Services.obs.addObserver(this, "SearchEngines:Remove", false);
+    Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
     Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
-    Services.obs.addObserver(this, "SearchEngines:Remove", false);
 
     let filter = {
       matches: function (aElement) {
         // Copied from body of isTargetAKeywordField function in nsContextMenu.js
         if(!(aElement instanceof HTMLInputElement))
           return false;
         let form = aElement.form;
         if (!form || aElement.type == "password")
@@ -6646,47 +6720,38 @@ var SearchEngines = {
       action: function(aElement) {
         SearchEngines.addEngine(aElement);
       }
     });
   },
 
   uninit: function uninit() {
     Services.obs.removeObserver(this, "SearchEngines:Add");
-    Services.obs.removeObserver(this, "SearchEngines:Get");
     Services.obs.removeObserver(this, "SearchEngines:GetVisible");
+    Services.obs.removeObserver(this, "SearchEngines:Remove");
+    Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults");
     Services.obs.removeObserver(this, "SearchEngines:SetDefault");
-    Services.obs.removeObserver(this, "SearchEngines:Remove");
     if (this._contextMenuId != null)
       NativeWindow.contextmenus.remove(this._contextMenuId);
   },
 
   // Fetch list of search engines. all ? All engines : Visible engines only.
-  _handleSearchEnginesGet: function _handleSearchEnginesGet(rv, all) {
+  _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
     if (!Components.isSuccessCode(rv)) {
       Cu.reportError("Could not initialize search service, bailing out.");
       return;
     }
-    let engineData;
-    if (all) {
-      engineData = Services.search.getEngines({});
-    } else {
-      engineData = Services.search.getVisibleEngines({});
-    }
-
-    // These engines are the bundled ones - they may not be uninstalled.
-    let immutableEngines = Services.search.getDefaultEngines();
-
+
+    let engineData = Services.search.getVisibleEngines({});
     let searchEngines = engineData.map(function (engine) {
       return {
         name: engine.name,
         identifier: engine.identifier,
         iconURI: (engine.iconURI ? engine.iconURI.spec : null),
-        hidden: engine.hidden,
-        immutable: immutableEngines.indexOf(engine) != -1
+        hidden: engine.hidden
       };
     });
 
     let suggestTemplate = null;
     let suggestEngine = null;
 
     // Check to see if the default engine supports search suggestions. We only need to check
     // the default engine because we only show suggestions for the default engine in the UI.
@@ -6713,55 +6778,49 @@ var SearchEngines = {
     let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
     let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
     try {
       connector.speculativeConnect(searchURI, callbacks);
     } catch (e) {}
   },
 
-  _handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) {
-    this._handleSearchEnginesGet(rv, true);
-  },
-  _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv) {
-    this._handleSearchEnginesGet(rv, false)
-  },
-
   // Helper method to extract the engine name from a JSON. Simplifies the observe function.
   _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
     let data = JSON.parse(aData);
     return Services.search.getEngineByName(data.engine);
   },
 
   observe: function observe(aSubject, aTopic, aData) {
     let engine;
     switch(aTopic) {
       case "SearchEngines:Add":
         this.displaySearchEnginesList(aData);
         break;
       case "SearchEngines:GetVisible":
         Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
         break;
-      case "SearchEngines:Get":
-        // Return a list of all engines, including "Hidden" ones.
-        Services.search.init(this._handleSearchEnginesGetAll.bind(this));
+      case "SearchEngines:Remove":
+        // Make sure the engine isn't hidden before removing it, to make sure it's
+        // visible if the user later re-adds it (works around bug 341833)
+        engine = this._extractEngineFromJSON(aData);
+        engine.hidden = false;
+        Services.search.removeEngine(engine);
+        break;
+      case "SearchEngines:RestoreDefaults":
+        // Un-hides all default engines.
+        Services.search.restoreDefaultEngines();
         break;
       case "SearchEngines:SetDefault":
         engine = this._extractEngineFromJSON(aData);
         // Move the new default search engine to the top of the search engine list.
         Services.search.moveEngine(engine, 0);
         Services.search.defaultEngine = engine;
         break;
-      case "SearchEngines:Remove":
-        // Make sure the engine isn't hidden before removing it, to make sure it's
-        // visible if the user later re-adds it (works around bug 341833)
-        engine = this._extractEngineFromJSON(aData);
-        engine.hidden = false;
-        Services.search.removeEngine(engine);
-        break;
+
       default:
         dump("Unexpected message type observed: " + aTopic);
         break;
     }
   },
 
   // Display context menu listing names of the search engines available to be added.
   displaySearchEnginesList: function displaySearchEnginesList(aData) {
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -218,17 +218,19 @@ SessionStore.prototype = {
       }
       case "TabSelect": {
         let browser = aEvent.target;
         this.onTabSelect(window, browser);
         break;
       }
       case "pageshow": {
         let browser = aEvent.currentTarget;
-        this.onTabLoad(window, browser, aEvent.persisted);
+        // Top-level changes only
+        if (aEvent.originalTarget == browser.contentDocument)
+          this.onTabLoad(window, browser, aEvent.persisted);
         break;
       }
     }
   },
 
   onWindowOpen: function ss_onWindowOpen(aWindow) {
     // Return if window has already been initialized
     if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID])
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -1,16 +1,17 @@
 /* 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";
 
 /* General utilities used throughout devtools. */
 
+let Cu = Components.utils;
 let { Promise: promise } = Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
 let { Services } = Components.utils.import("resource://gre/modules/Services.jsm", {});
 
 /**
  * Turn the error |aError| into a string, without fail.
  */
 this.safeErrorString = function safeErrorString(aError) {
   try {
@@ -232,8 +233,29 @@ this.getProperty = function getProperty(
  * @return Boolean
  *         Whether a safe getter was found.
  */
 this.hasSafeGetter = function hasSafeGetter(aDesc) {
   let fn = aDesc.get;
   return fn && fn.callable && fn.class == "Function" && fn.script === undefined;
 };
 
+/**
+ * Check if it is safe to read properties and execute methods from the given JS
+ * object. Safety is defined as being protected from unintended code execution
+ * from content scripts (or cross-compartment code).
+ *
+ * See bugs 945920 and 946752 for discussion.
+ *
+ * @type Object aObj
+ *       The object to check.
+ * @return Boolean
+ *         True if it is safe to read properties from aObj, or false otherwise.
+ */
+this.isSafeJSObject = function isSafeJSObject(aObj) {
+  if (Cu.getGlobalForObject(aObj) ==
+      Cu.getGlobalForObject(isSafeJSObject)) {
+    return true; // aObj is not a cross-compartment wrapper.
+  }
+
+  return Cu.isXrayWrapper(aObj);
+};
+
--- a/toolkit/devtools/DevToolsUtils.jsm
+++ b/toolkit/devtools/DevToolsUtils.jsm
@@ -23,9 +23,10 @@ this.DevToolsUtils = {
   reportException: reportException,
   makeInfallible: makeInfallible,
   zip: zip,
   yieldingEach: yieldingEach,
   reportingDisabled: false , // Used by tests.
   defineLazyPrototypeGetter: defineLazyPrototypeGetter,
   getProperty: getProperty,
   hasSafeGetter: hasSafeGetter,
+  isSafeJSObject: isSafeJSObject,
 };
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1,16 +1,24 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 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";
 
+let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
+      "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
+      "Float64Array"];
+
+// Number of items to preview in objects, arrays, maps, sets, lists,
+// collections, etc.
+let OBJECT_PREVIEW_MAX_ITEMS = 10;
+
 /**
  * BreakpointStore objects keep track of all breakpoints that get set so that we
  * can reset them when the same script is introduced to the thread again (such
  * as after a refresh).
  */
 function BreakpointStore() {
   // If we have a whole-line breakpoint set at LINE in URL, then
   //
@@ -435,25 +443,30 @@ function ThreadActor(aHooks, aGlobal)
   this.findGlobals = this.globalManager.findGlobals.bind(this);
   this.onNewGlobal = this.globalManager.onNewGlobal.bind(this);
   this.onNewSource = this.onNewSource.bind(this);
   this._allEventsListener = this._allEventsListener.bind(this);
 
   this._options = {
     useSourceMaps: false
   };
+
+  this._gripDepth = 0;
 }
 
 /**
  * The breakpoint store must be shared across instances of ThreadActor so that
  * page reloads don't blow away all of our breakpoints.
  */
 ThreadActor.breakpointStore = new BreakpointStore();
 
 ThreadActor.prototype = {
+  // Used by the ObjectActor to keep track of the depth of grip() calls.
+  _gripDepth: null,
+
   actorPrefix: "context",
 
   get state() { return this._state; },
   get attached() this.state == "attached" ||
                  this.state == "running" ||
                  this.state == "paused",
 
   get breakpointStore() { return ThreadActor.breakpointStore; },
@@ -2725,17 +2738,17 @@ function errorStringify(aObj) {
  * Stringify a Debugger.Object based on its class.
  *
  * @param Debugger.Object aObj
  *        The object to stringify.
  * @return String
  *         The stringification for the object.
  */
 function stringify(aObj) {
-  if (Cu.isDeadWrapper(aObj)) {
+  if (aObj.class == "DeadObject") {
     const error = new Error("Dead object encountered.");
     DevToolsUtils.reportException("stringify", error);
     return "<dead object>";
   }
   const stringifier = stringifiers[aObj.class] || stringifiers.Object;
   return stringifier(aObj);
 }
 
@@ -2824,48 +2837,43 @@ function ObjectActor(aObj, aThreadActor)
 
 ObjectActor.prototype = {
   actorPrefix: "obj",
 
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
   grip: function () {
+    this.threadActor._gripDepth++;
+
     let g = {
       "type": "object",
       "class": this.obj.class,
       "actor": this.actorID,
       "extensible": this.obj.isExtensible(),
       "frozen": this.obj.isFrozen(),
       "sealed": this.obj.isSealed()
     };
 
-    // Add additional properties for functions.
-    if (this.obj.class === "Function") {
-      if (this.obj.name) {
-        g.name = this.obj.name;
-      }
-      if (this.obj.displayName) {
-        g.displayName = this.obj.displayName;
+    if (this.obj.class != "DeadObject") {
+      let raw = Cu.unwaiveXrays(this.obj.unsafeDereference());
+      if (!DevToolsUtils.isSafeJSObject(raw)) {
+        raw = null;
       }
 
-      // Check if the developer has added a de-facto standard displayName
-      // property for us to use.
-      try {
-        let desc = this.obj.getOwnPropertyDescriptor("displayName");
-        if (desc && desc.value && typeof desc.value == "string") {
-          g.userDisplayName = this.threadActor.createValueGrip(desc.value);
+      let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] ||
+                       DebuggerServer.ObjectActorPreviewers.Object;
+      for (let fn of previewers) {
+        if (fn(this, g, raw)) {
+          break;
         }
-      } catch (e) {
-        // Calling getOwnPropertyDescriptor with displayName might throw
-        // with "permission denied" errors for some functions.
-        dumpn(e);
       }
     }
 
+    this.threadActor._gripDepth--;
     return g;
   },
 
   /**
    * Releases this actor from the pool.
    */
   release: function () {
     if (this.registeredPool.objectActors) {
@@ -2959,25 +2967,27 @@ ObjectActor.prototype = {
 
   /**
    * Find the safe getter values for the current Debugger.Object, |this.obj|.
    *
    * @private
    * @param object aOwnProperties
    *        The object that holds the list of known ownProperties for
    *        |this.obj|.
+   * @param number [aLimit=0]
+   *        Optional limit of getter values to find.
    * @return object
    *         An object that maps property names to safe getter descriptors as
    *         defined by the remote debugging protocol.
    */
-  _findSafeGetterValues: function (aOwnProperties)
+  _findSafeGetterValues: function (aOwnProperties, aLimit = 0)
   {
     let safeGetterValues = Object.create(null);
     let obj = this.obj;
-    let level = 0;
+    let level = 0, i = 0;
 
     while (obj) {
       let getters = this._findSafeGetters(obj);
       for (let name of getters) {
         // Avoid overwriting properties from prototypes closer to this.obj. Also
         // avoid providing safeGetterValues from prototypes if property |name|
         // is already defined as an own property.
         if (name in safeGetterValues ||
@@ -3009,19 +3019,25 @@ ObjectActor.prototype = {
           // return undefined and should be ignored.
           if (getterValue !== undefined) {
             safeGetterValues[name] = {
               getterValue: this.threadActor.createValueGrip(getterValue),
               getterPrototypeLevel: level,
               enumerable: desc.enumerable,
               writable: level == 0 ? desc.writable : true,
             };
+            if (aLimit && ++i == aLimit) {
+              break;
+            }
           }
         }
       }
+      if (aLimit && i == aLimit) {
+        break;
+      }
 
       obj = obj.proto;
       level++;
     }
 
     return safeGetterValues;
   },
 
@@ -3038,17 +3054,25 @@ ObjectActor.prototype = {
    */
   _findSafeGetters: function (aObject)
   {
     if (aObject._safeGetters) {
       return aObject._safeGetters;
     }
 
     let getters = new Set();
-    for (let name of aObject.getOwnPropertyNames()) {
+    let names = [];
+    try {
+      names = aObject.getOwnPropertyNames()
+    } catch (ex) {
+      // Calling getOwnPropertyNames() on some wrapped native prototypes is not
+      // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
+    }
+
+    for (let name of names) {
       let desc = null;
       try {
         desc = aObject.getOwnPropertyDescriptor(name);
       } catch (e) {
         // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
         // allowed (bug 560072).
       }
       if (!desc || desc.value !== undefined || !("get" in desc)) {
@@ -3103,36 +3127,43 @@ ObjectActor.prototype = {
     return { from: this.actorID,
              displayString: this.threadActor.createValueGrip(string) };
   },
 
   /**
    * A helper method that creates a property descriptor for the provided object,
    * properly formatted for sending in a protocol response.
    *
+   * @private
    * @param string aName
    *        The property that the descriptor is generated for.
+   * @param boolean [aOnlyEnumerable]
+   *        Optional: true if you want a descriptor only for an enumerable
+   *        property, false otherwise.
+   * @return object|undefined
+   *         The property descriptor, or undefined if this is not an enumerable
+   *         property and aOnlyEnumerable=true.
    */
-  _propertyDescriptor: function (aName) {
+  _propertyDescriptor: function (aName, aOnlyEnumerable) {
     let desc;
     try {
       desc = this.obj.getOwnPropertyDescriptor(aName);
     } catch (e) {
       // Calling getOwnPropertyDescriptor on wrapped native prototypes is not
       // allowed (bug 560072). Inform the user with a bogus, but hopefully
       // explanatory, descriptor.
       return {
         configurable: false,
         writable: false,
         enumerable: false,
         value: e.name
       };
     }
 
-    if (!desc) {
+    if (!desc || aOnlyEnumerable && !desc.enumerable) {
       return undefined;
     }
 
     let retval = {
       configurable: desc.configurable,
       enumerable: desc.enumerable
     };
 
@@ -3228,16 +3259,575 @@ ObjectActor.prototype.requestTypes = {
   "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
   "decompile": ObjectActor.prototype.onDecompile,
   "release": ObjectActor.prototype.onRelease,
   "scope": ObjectActor.prototype.onScope,
 };
 
 
 /**
+ * Functions for adding information to ObjectActor grips for the purpose of
+ * having customized output. This object holds arrays mapped by
+ * Debugger.Object.prototype.class.
+ *
+ * In each array you can add functions that take two
+ * arguments:
+ *   - the ObjectActor instance to make a preview for,
+ *   - the grip object being prepared for the client,
+ *   - the raw JS object after calling Debugger.Object.unsafeDereference(). This
+ *   argument is only provided if the object is safe for reading properties and
+ *   executing methods. See DevToolsUtils.isSafeJSObject().
+ *
+ * Functions must return false if they cannot provide preview
+ * information for the debugger object, or true otherwise.
+ */
+DebuggerServer.ObjectActorPreviewers = {
+  Function: [function({obj, threadActor}, aGrip) {
+    if (obj.name) {
+      aGrip.name = obj.name;
+    }
+
+    if (obj.displayName) {
+      aGrip.displayName = obj.displayName.substr(0, 500);
+    }
+
+    if (obj.parameterNames) {
+      aGrip.parameterNames = obj.parameterNames;
+    }
+
+    // Check if the developer has added a de-facto standard displayName
+    // property for us to use.
+    let userDisplayName;
+    try {
+      userDisplayName = obj.getOwnPropertyDescriptor("displayName");
+    } catch (e) {
+      // Calling getOwnPropertyDescriptor with displayName might throw
+      // with "permission denied" errors for some functions.
+      dumpn(e);
+    }
+
+    if (userDisplayName && typeof userDisplayName.value == "string" &&
+        userDisplayName.value) {
+      aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value);
+    }
+
+    return true;
+  }],
+
+  RegExp: [function({obj, threadActor}, aGrip) {
+    // Avoid having any special preview for the RegExp.prototype itself.
+    if (!obj.proto || obj.proto.class != "RegExp") {
+      return false;
+    }
+
+    let str = RegExp.prototype.toString.call(obj.unsafeDereference());
+    aGrip.displayString = threadActor.createValueGrip(str);
+    return true;
+  }],
+
+  Date: [function({obj, threadActor}, aGrip) {
+    if (!obj.proto || obj.proto.class != "Date") {
+      return false;
+    }
+
+    let time = Date.prototype.getTime.call(obj.unsafeDereference());
+
+    aGrip.preview = {
+      timestamp: threadActor.createValueGrip(time),
+    };
+    return true;
+  }],
+
+  Array: [function({obj, threadActor}, aGrip) {
+    let length = DevToolsUtils.getProperty(obj, "length");
+    if (typeof length != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let items = aGrip.preview.items = [];
+
+    for (let [i, value] of Array.prototype.entries.call(raw)) {
+      if (Object.hasOwnProperty.call(raw, i)) {
+        value = makeDebuggeeValueIfNeeded(obj, value);
+        items.push(threadActor.createValueGrip(value));
+      } else {
+        items.push(null);
+      }
+
+      if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // Array
+
+  Set: [function({obj, threadActor}, aGrip) {
+    let size = DevToolsUtils.getProperty(obj, "size");
+    if (typeof size != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: size,
+    };
+
+    // Avoid recursive object grips.
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let items = aGrip.preview.items = [];
+    for (let item of Set.prototype.values.call(raw)) {
+      item = makeDebuggeeValueIfNeeded(obj, item);
+      items.push(threadActor.createValueGrip(item));
+      if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // Set
+
+  Map: [function({obj, threadActor}, aGrip) {
+    let size = DevToolsUtils.getProperty(obj, "size");
+    if (typeof size != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "MapLike",
+      size: size,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let entries = aGrip.preview.entries = [];
+    for (let [key, value] of Map.prototype.entries.call(raw)) {
+      key = makeDebuggeeValueIfNeeded(obj, key);
+      value = makeDebuggeeValueIfNeeded(obj, value);
+      entries.push([threadActor.createValueGrip(key),
+                    threadActor.createValueGrip(value)]);
+      if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // Map
+
+  DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj) {
+      return false;
+    }
+
+    let keys = obj.getOwnPropertyNames();
+    aGrip.preview = {
+      kind: "MapLike",
+      size: keys.length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let entries = aGrip.preview.entries = [];
+    for (let key of keys) {
+      let value = makeDebuggeeValueIfNeeded(obj, aRawObj[key]);
+      entries.push([key, threadActor.createValueGrip(value)]);
+      if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    return true;
+  }], // DOMStringMap
+}; // DebuggerServer.ObjectActorPreviewers
+
+// Preview functions that do not rely on the object class.
+DebuggerServer.ObjectActorPreviewers.Object = [
+  function TypedArray({obj, threadActor}, aGrip) {
+    if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) {
+      return false;
+    }
+
+    let length = DevToolsUtils.getProperty(obj, "length");
+    if (typeof length != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let raw = obj.unsafeDereference();
+    let global = Cu.getGlobalForObject(DebuggerServer);
+    let classProto = global[obj.class].prototype;
+    let safeView = classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS);
+    let items = aGrip.preview.items = [];
+    for (let i = 0; i < safeView.length; i++) {
+      items.push(safeView[i]);
+    }
+
+    return true;
+  },
+
+  function Error({obj, threadActor}, aGrip) {
+    switch (obj.class) {
+      case "Error":
+      case "EvalError":
+      case "RangeError":
+      case "ReferenceError":
+      case "SyntaxError":
+      case "TypeError":
+      case "URIError":
+        let name = DevToolsUtils.getProperty(obj, "name");
+        let msg = DevToolsUtils.getProperty(obj, "message");
+        let stack = DevToolsUtils.getProperty(obj, "stack");
+        let fileName = DevToolsUtils.getProperty(obj, "fileName");
+        let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber");
+        let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber");
+        aGrip.preview = {
+          kind: "Error",
+          name: threadActor.createValueGrip(name),
+          message: threadActor.createValueGrip(msg),
+          stack: threadActor.createValueGrip(stack),
+          fileName: threadActor.createValueGrip(fileName),
+          lineNumber: threadActor.createValueGrip(lineNumber),
+          columnNumber: threadActor.createValueGrip(columnNumber),
+        };
+        return true;
+      default:
+        return false;
+    }
+  },
+
+  function CSSMediaRule({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSMediaRule)) {
+      return false;
+    }
+    aGrip.preview = {
+      kind: "ObjectWithText",
+      text: threadActor.createValueGrip(aRawObj.conditionText),
+    };
+    return true;
+  },
+
+  function CSSStyleRule({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleRule)) {
+      return false;
+    }
+    aGrip.preview = {
+      kind: "ObjectWithText",
+      text: threadActor.createValueGrip(aRawObj.selectorText),
+    };
+    return true;
+  },
+
+  function ObjectWithURL({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj ||
+        !(aRawObj instanceof Ci.nsIDOMCSSImportRule ||
+          aRawObj instanceof Ci.nsIDOMCSSStyleSheet ||
+          aRawObj instanceof Ci.nsIDOMLocation ||
+          aRawObj instanceof Ci.nsIDOMWindow)) {
+      return false;
+    }
+
+    let url;
+    if (aRawObj instanceof Ci.nsIDOMWindow) {
+      url = aRawObj.location.href;
+    } else {
+      url = aRawObj.href;
+    }
+
+    aGrip.preview = {
+      kind: "ObjectWithURL",
+      url: threadActor.createValueGrip(url),
+    };
+
+    return true;
+  },
+
+  function ArrayLike({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj ||
+        obj.class != "DOMTokenList" &&
+        !(aRawObj instanceof Ci.nsIDOMMozNamedAttrMap ||
+          aRawObj instanceof Ci.nsIDOMCSSRuleList ||
+          aRawObj instanceof Ci.nsIDOMCSSValueList ||
+          aRawObj instanceof Ci.nsIDOMDOMStringList ||
+          aRawObj instanceof Ci.nsIDOMFileList ||
+          aRawObj instanceof Ci.nsIDOMFontFaceList ||
+          aRawObj instanceof Ci.nsIDOMMediaList ||
+          aRawObj instanceof Ci.nsIDOMNodeList ||
+          aRawObj instanceof Ci.nsIDOMStyleSheetList)) {
+      return false;
+    }
+
+    if (typeof aRawObj.length != "number") {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "ArrayLike",
+      length: aRawObj.length,
+    };
+
+    if (threadActor._gripDepth > 1) {
+      return true;
+    }
+
+    let items = aGrip.preview.items = [];
+
+    for (let i = 0; i < aRawObj.length &&
+                    items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) {
+      let value = makeDebuggeeValueIfNeeded(obj, aRawObj[i]);
+      items.push(threadActor.createValueGrip(value));
+    }
+
+    return true;
+  }, // ArrayLike
+
+  function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "MapLike",
+      size: aRawObj.length,
+    };
+
+    let entries = aGrip.preview.entries = [];
+
+    for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS &&
+                    i < aRawObj.length; i++) {
+      let prop = aRawObj[i];
+      let value = aRawObj.getPropertyValue(prop);
+      entries.push([prop, threadActor.createValueGrip(value)]);
+    }
+
+    return true;
+  },
+
+  function DOMNode({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMNode)) {
+      return false;
+    }
+
+    let preview = aGrip.preview = {
+      kind: "DOMNode",
+      nodeType: aRawObj.nodeType,
+      nodeName: aRawObj.nodeName,
+    };
+
+    if (aRawObj instanceof Ci.nsIDOMDocument) {
+      preview.location = threadActor.createValueGrip(aRawObj.location.href);
+    } else if (aRawObj instanceof Ci.nsIDOMDocumentFragment) {
+      preview.childNodesLength = aRawObj.childNodes.length;
+
+      if (threadActor._gripDepth < 2) {
+        preview.childNodes = [];
+        for (let node of aRawObj.childNodes) {
+          let actor = threadActor.createValueGrip(obj.makeDebuggeeValue(node));
+          preview.childNodes.push(actor);
+          if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) {
+            break;
+          }
+        }
+      }
+    } else if (aRawObj instanceof Ci.nsIDOMElement) {
+      // Add preview for DOM element attributes.
+      if (aRawObj instanceof Ci.nsIDOMHTMLElement) {
+        preview.nodeName = preview.nodeName.toLowerCase();
+      }
+
+      let i = 0;
+      preview.attributes = {};
+      preview.attributesLength = aRawObj.attributes.length;
+      for (let attr of aRawObj.attributes) {
+        preview.attributes[attr.nodeName] = threadActor.createValueGrip(attr.value);
+        if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
+          break;
+        }
+      }
+    } else if (aRawObj instanceof Ci.nsIDOMAttr) {
+      preview.value = threadActor.createValueGrip(aRawObj.value);
+    } else if (aRawObj instanceof Ci.nsIDOMText ||
+               aRawObj instanceof Ci.nsIDOMComment) {
+      preview.textContent = threadActor.createValueGrip(aRawObj.textContent);
+    }
+
+    return true;
+  }, // DOMNode
+
+  function DOMEvent({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) {
+      return false;
+    }
+
+    let preview = aGrip.preview = {
+      kind: "DOMEvent",
+      type: aRawObj.type,
+      properties: Object.create(null),
+    };
+
+    if (threadActor._gripDepth < 2) {
+      let target = obj.makeDebuggeeValue(aRawObj.target);
+      preview.target = threadActor.createValueGrip(target);
+    }
+
+    let props = [];
+    if (aRawObj instanceof Ci.nsIDOMMouseEvent) {
+      props.push("buttons", "clientX", "clientY", "layerX", "layerY");
+    } else if (aRawObj instanceof Ci.nsIDOMKeyEvent) {
+      let modifiers = [];
+      if (aRawObj.altKey) {
+        modifiers.push("Alt");
+      }
+      if (aRawObj.ctrlKey) {
+        modifiers.push("Control");
+      }
+      if (aRawObj.metaKey) {
+        modifiers.push("Meta");
+      }
+      if (aRawObj.shiftKey) {
+        modifiers.push("Shift");
+      }
+      preview.eventKind = "key";
+      preview.modifiers = modifiers;
+
+      props.push("key", "charCode", "keyCode");
+    } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent ||
+               aRawObj instanceof Ci.nsIDOMAnimationEvent) {
+      props.push("animationName", "pseudoElement");
+    } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) {
+      props.push("clipboardData");
+    }
+
+    // Add event-specific properties.
+    for (let prop of props) {
+      let value = aRawObj[prop];
+      if (value && (typeof value == "object" || typeof value == "function")) {
+        // Skip properties pointing to objects.
+        if (threadActor._gripDepth > 1) {
+          continue;
+        }
+        value = obj.makeDebuggeeValue(value);
+      }
+      preview.properties[prop] = threadActor.createValueGrip(value);
+    }
+
+    // Add any properties we find on the event object.
+    if (!props.length) {
+      let i = 0;
+      for (let prop in aRawObj) {
+        let value = aRawObj[prop];
+        if (prop == "target" || prop == "type" || value === null ||
+            typeof value == "function") {
+          continue;
+        }
+        if (value && typeof value == "object") {
+          if (threadActor._gripDepth > 1) {
+            continue;
+          }
+          value = obj.makeDebuggeeValue(value);
+        }
+        preview.properties[prop] = threadActor.createValueGrip(value);
+        if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
+          break;
+        }
+      }
+    }
+
+    return true;
+  }, // DOMEvent
+
+  function DOMException({obj, threadActor}, aGrip, aRawObj) {
+    if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) {
+      return false;
+    }
+
+    aGrip.preview = {
+      kind: "DOMException",
+      name: threadActor.createValueGrip(aRawObj.name),
+      message: threadActor.createValueGrip(aRawObj.message),
+      code: threadActor.createValueGrip(aRawObj.code),
+      result: threadActor.createValueGrip(aRawObj.result),
+      filename: threadActor.createValueGrip(aRawObj.filename),
+      lineNumber: threadActor.createValueGrip(aRawObj.lineNumber),
+      columnNumber: threadActor.createValueGrip(aRawObj.columnNumber),
+    };
+
+    return true;
+  },
+
+  function GenericObject(aObjectActor, aGrip) {
+    let {obj, threadActor} = aObjectActor;
+    if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) {
+      return false;
+    }
+
+    let i = 0, names = [];
+    let preview = aGrip.preview = {
+      kind: "Object",
+      ownProperties: Object.create(null),
+    };
+
+    try {
+      names = obj.getOwnPropertyNames();
+    } catch (ex) {
+      // Calling getOwnPropertyNames() on some wrapped native prototypes is not
+      // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
+    }
+
+    preview.ownPropertiesLength = names.length;
+
+    for (let name of names) {
+      let desc = aObjectActor._propertyDescriptor(name, true);
+      if (!desc) {
+        continue;
+      }
+
+      preview.ownProperties[name] = desc;
+      if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
+        break;
+      }
+    }
+
+    if (i < OBJECT_PREVIEW_MAX_ITEMS) {
+      preview.safeGetterValues = aObjectActor.
+                                 _findSafeGetterValues(preview.ownProperties,
+                                                       OBJECT_PREVIEW_MAX_ITEMS - i);
+    }
+
+    return true;
+  }, // GenericObject
+]; // DebuggerServer.ObjectActorPreviewers.Object
+
+/**
  * Creates a pause-scoped actor for the specified object.
  * @see ObjectActor
  */
 function PauseScopedObjectActor()
 {
   ObjectActor.apply(this, arguments);
 }
 
@@ -4512,8 +5102,28 @@ function findCssSelector(ele) {
 function positionInNodeList(element, nodeList) {
   for (var i = 0; i < nodeList.length; i++) {
     if (element === nodeList[i]) {
       return i;
     }
   }
   return -1;
 }
+
+/**
+ * Make a debuggee value for the given object, if needed. Primitive values
+ * are left the same.
+ *
+ * Use case: you have a raw JS object (after unsafe dereference) and you want to
+ * send it to the client. In that case you need to use an ObjectActor which
+ * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue()
+ * method works only for JS objects and functions.
+ *
+ * @param Debugger.Object obj
+ * @param any value
+ * @return object
+ */
+function makeDebuggeeValueIfNeeded(obj, value) {
+  if (value && (typeof value == "object" || typeof value == "function")) {
+    return obj.makeDebuggeeValue(value);
+  }
+  return value;
+}
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -56,16 +56,17 @@ function WebConsoleActor(aConnection, aP
   this.conn.addActorPool(this._actorPool);
 
   this._prefs = {};
 
   this.dbg = new Debugger();
 
   this._protoChains = new Map();
   this._netEvents = new Map();
+  this._gripDepth = 0;
 
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited", false);
   }
 }
 
@@ -76,16 +77,23 @@ WebConsoleActor.prototype =
   /**
    * Debugger instance.
    *
    * @see jsdebugger.jsm
    */
   dbg: null,
 
   /**
+   * This is used by the ObjectActor to keep track of the depth of grip() calls.
+   * @private
+   * @type number
+   */
+  _gripDepth: null,
+
+  /**
    * Actor pool for all of the actors we send to the client.
    * @private
    * @type object
    * @see ActorPool
    */
   _actorPool: null,
 
   /**
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -183,22 +183,28 @@ let WebConsoleUtils = {
   },
 
   /**
    * Abbreviates the given source URL so that it can be displayed flush-right
    * without being too distracting.
    *
    * @param string aSourceURL
    *        The source URL to shorten.
+   * @param object [aOptions]
+   *        Options:
+   *        - onlyCropQuery: boolean that tells if the URL abbreviation function
+   *        should only remove the query parameters and the hash fragment from
+   *        the given URL.
    * @return string
    *         The abbreviated form of the source URL.
    */
-  abbreviateSourceURL: function WCU_abbreviateSourceURL(aSourceURL)
+  abbreviateSourceURL:
+  function WCU_abbreviateSourceURL(aSourceURL, aOptions = {})
   {
-    if (aSourceURL.substr(0, 5) == "data:") {
+    if (!aOptions.onlyCropQuery && aSourceURL.substr(0, 5) == "data:") {
       let commaIndex = aSourceURL.indexOf(",");
       if (commaIndex > -1) {
         aSourceURL = "data:" + aSourceURL.substring(commaIndex + 1);
       }
     }
 
     // Remove any query parameters.
     let hookIndex = aSourceURL.indexOf("?");
@@ -209,23 +215,25 @@ let WebConsoleUtils = {
     // Remove any hash fragments.
     let hashIndex = aSourceURL.indexOf("#");
     if (hashIndex > -1) {
       aSourceURL = aSourceURL.substring(0, hashIndex);
     }
 
     // Remove a trailing "/".
     if (aSourceURL[aSourceURL.length - 1] == "/") {
-      aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
+      aSourceURL = aSourceURL.replace(/\/+$/, "");
     }
 
     // Remove all but the last path component.
-    let slashIndex = aSourceURL.lastIndexOf("/");
-    if (slashIndex > -1) {
-      aSourceURL = aSourceURL.substring(slashIndex + 1);
+    if (!aOptions.onlyCropQuery) {
+      let slashIndex = aSourceURL.lastIndexOf("/");
+      if (slashIndex > -1) {
+        aSourceURL = aSourceURL.substring(slashIndex + 1);
+      }
     }
 
     return aSourceURL;
   },
 
   /**
    * Tells if the given function is native or not.
    *
--- a/toolkit/mozapps/extensions/DeferredSave.jsm
+++ b/toolkit/mozapps/extensions/DeferredSave.jsm
@@ -6,16 +6,19 @@
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
+// Make it possible to mock out timers for testing
+let MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
 this.EXPORTED_SYMBOLS = ["DeferredSave"];
 
 // If delay parameter is not provided, default is 50 milliseconds.
 const DEFAULT_SAVE_DELAY_MS = 50;
 
 /**
  * A module to manage deferred, asynchronous writing of data files
  * to disk. Writing is deferred by waiting for a specified delay after
@@ -99,17 +102,17 @@ this.DeferredSave.prototype = {
   // Start the pending timer if data is dirty
   _startTimer: function() {
     if (!this._pending) {
       return;
     }
 
     this.LOG("Starting timer");
     if (!this._timer)
-      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this._timer = MakeTimer();
     this._timer.initWithCallback(() => this._deferredSave(),
                                  this._delay, Ci.nsITimer.TYPE_ONE_SHOT);
   },
 
   /**
    * Mark the current stored data dirty, and schedule a flush to disk
    * @return A Promise<integer> that will be resolved after the data is written to disk;
    *         the promise is resolved with the number of bytes written.
--- a/toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js
@@ -7,21 +7,21 @@
 
 "use strict";
 
 const testFile = gProfD.clone();
 testFile.append("DeferredSaveTest");
 
 Components.utils.import("resource://gre/modules/Promise.jsm");
 
-let context = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {});
-let DeferredSave = context.DeferredSave;
+let DSContext = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {});
+let DeferredSave = DSContext.DeferredSave;
 
-// Test wrapper to let us do promise/task based testing of DeferredSaveP
-function DeferredSaveTester(aDelay, aDataProvider) {
+// Test wrapper to let us do promise/task based testing of DeferredSave
+function DeferredSaveTester(aDataProvider) {
   let tester = {
     // Deferred for the promise returned by the mock writeAtomic
     waDeferred: null,
 
     // The most recent data "written" by the mock OS.File.writeAtomic
     writtenData: undefined,
 
     dataToSave: "Data to save",
@@ -49,58 +49,124 @@ function DeferredSaveTester(aDelay, aDat
     do_print("default write callback");
     let length = aTester.writtenData.length;
     do_execute_soon(() => aTester.waDeferred.resolve(length));
   }
 
   if (!aDataProvider)
     aDataProvider = () => tester.dataToSave;
 
-  tester.saver = new DeferredSave(testFile.path, aDataProvider, aDelay);
+  tester.saver = new DeferredSave(testFile.path, aDataProvider);
 
   // Install a mock for OS.File.writeAtomic to let us control the async
   // behaviour of the promise
-  context.OS.File.writeAtomic = function mock_writeAtomic(aFile, aData, aOptions) {
+  DSContext.OS.File.writeAtomic = function mock_writeAtomic(aFile, aData, aOptions) {
       do_print("writeAtomic: " + aFile + " data: '" + aData + "', " + aOptions.toSource());
       tester.writtenData = aData;
       tester.waDeferred = Promise.defer();
       tester.writeHandler(tester);
       return tester.waDeferred.promise;
     };
 
   return tester;
 };
 
 /**
+ * Install a mock nsITimer factory that triggers on the next spin of
+ * the event loop after it is scheduled
+ */
+function setQuickMockTimer() {
+  let quickTimer = {
+    initWithCallback: function(aFunction, aDelay, aType) {
+      do_print("Starting quick timer, delay = " + aDelay);
+      do_execute_soon(aFunction);
+    },
+    cancel: function() {
+      do_throw("Attempted to cancel a quickMockTimer");
+    }
+  };
+  DSContext.MakeTimer = () => {
+    do_print("Creating quick timer");
+    return quickTimer;
+  };
+}
+
+/**
+ * Install a mock nsITimer factory in DeferredSave.jsm, returning a promise that resolves
+ * when the client code sets the timer. Test cases can use this to wait for client code to
+ * be ready for a timer event, and then signal the event by calling mockTimer.callback().
+ * This could use some enhancement; clients can re-use the returned timer,
+ * but with this implementation it's not possible for the test to wait for
+ * a second call to initWithCallback() on the re-used timer.
+ * @return Promise{mockTimer} that resolves when initWithCallback()
+ *         is called
+ */
+function setPromiseMockTimer() {
+  let waiter = Promise.defer();
+  let mockTimer = {
+    callback: null,
+    delay: null,
+    type: null,
+    isCancelled: false,
+
+    initWithCallback: function(aFunction, aDelay, aType) {
+      do_print("Starting timer, delay = " + aDelay);
+      this.callback = aFunction;
+      this.delay = aDelay;
+      this.type = aType;
+      // cancelled timers can be re-used
+      this.isCancelled = false;
+      waiter.resolve(this);
+    },
+    cancel: function() {
+      do_print("Cancelled mock timer");
+      this.callback = null;
+      this.delay = null;
+      this.type = null;
+      this.isCancelled = true;
+      // If initWithCallback was never called, resolve to let tests check for cancel
+      waiter.resolve(this);
+    }
+  };
+  DSContext.MakeTimer = () => {
+    do_print("Creating mock timer");
+    return mockTimer;
+  };
+  return waiter.promise;
+}
+
+/**
  * Return a Promise<null> that resolves after the specified number of milliseconds
  */
 function delay(aDelayMS) {
   let deferred = Promise.defer();
   do_timeout(aDelayMS, () => deferred.resolve(null));
   return deferred.promise;
 }
 
 function run_test() {
   run_next_test();
 }
 
 // Modify set data once, ask for save, make sure it saves cleanly
 add_task(function test_basic_save_succeeds() {
-  let tester = DeferredSaveTester(1);
+  setQuickMockTimer();
+  let tester = DeferredSaveTester();
   let data = "Test 1 Data";
 
   yield tester.save(data);
   do_check_eq(tester.writtenData, data);
   do_check_eq(1, tester.saver.totalSaves);
 });
 
 // Two saves called during the same event loop, both with callbacks
 // Make sure we save only the second version of the data
 add_task(function test_two_saves() {
-  let tester = DeferredSaveTester(1);
+  setQuickMockTimer();
+  let tester = DeferredSaveTester();
   let firstCallback_happened = false;
   let firstData = "Test first save";
   let secondData = "Test second save";
 
   // first save should not resolve until after the second one is called,
   // so we can't just yield this promise
   tester.save(firstData).then(count => {
     do_check_eq(secondData, tester.writtenData);
@@ -112,49 +178,60 @@ add_task(function test_two_saves() {
   do_check_true(firstCallback_happened);
   do_check_eq(secondData, tester.writtenData);
   do_check_eq(1, tester.saver.totalSaves);
 });
 
 // Two saves called with a delay in between, both with callbacks
 // Make sure we save the second version of the data
 add_task(function test_two_saves_delay() {
-  let tester = DeferredSaveTester(50);
+  let timerPromise = setPromiseMockTimer();
+  let tester = DeferredSaveTester();
   let firstCallback_happened = false;
   let delayDone = false;
 
   let firstData = "First data to save with delay";
   let secondData = "Modified data to save with delay";
 
   tester.save(firstData).then(count => {
     do_check_false(firstCallback_happened);
     do_check_true(delayDone);
     do_check_eq(secondData, tester.writtenData);
     firstCallback_happened = true;
   }, do_report_unexpected_exception);
 
-  yield delay(5);
+  // Wait a short time to let async events possibly spawned by the
+  // first tester.save() to run
+  yield delay(2);
   delayDone = true;
-  yield tester.save(secondData);
+  // request to save modified data
+  let saving = tester.save(secondData);
+  // Yield to wait for client code to set the timer
+  let activeTimer = yield timerPromise;
+  // and then trigger it
+  activeTimer.callback();
+  // now wait for the DeferredSave to finish saving
+  yield saving;
   do_check_true(firstCallback_happened);
   do_check_eq(secondData, tester.writtenData);
   do_check_eq(1, tester.saver.totalSaves);
   do_check_eq(0, tester.saver.overlappedSaves);
 });
 
 // Test case where OS.File immediately reports an error when the write begins
 // Also check that the "error" getter correctly returns the error
 // Then do a write that succeeds, and make sure the error is cleared
 add_task(function test_error_immediate() {
-  let tester = DeferredSaveTester(1);
+  let tester = DeferredSaveTester();
   let testError = new Error("Forced failure");
   function writeFail(aTester) {
     aTester.waDeferred.reject(testError);
   }
 
+  setQuickMockTimer();
   yield tester.save("test_error_immediate", writeFail).then(
     count => do_throw("Did not get expected error"),
     error => do_check_eq(testError.message, error.message)
     );
   do_check_eq(testError, tester.lastError);
 
   // This write should succeed and clear the error
   yield tester.save("test_error_immediate succeeds");
@@ -162,28 +239,29 @@ add_task(function test_error_immediate()
   // The failed save attempt counts in our total
   do_check_eq(2, tester.saver.totalSaves);
 });
 
 // Save one set of changes, then while the write is in progress, modify the
 // data two more times. Test that we re-write the dirty data exactly once
 // after the first write succeeds
 add_task(function dirty_while_writing() {
-  let tester = DeferredSaveTester(1);
+  let tester = DeferredSaveTester();
   let firstData = "First data";
   let secondData = "Second data";
   let thirdData = "Third data";
   let firstCallback_happened = false;
   let secondCallback_happened = false;
   let writeStarted = Promise.defer();
 
   function writeCallback(aTester) {
     writeStarted.resolve(aTester.waDeferred);
   }
 
+  setQuickMockTimer();
   do_print("First save");
   tester.save(firstData, writeCallback).then(
     count => {
       do_check_false(firstCallback_happened);
       do_check_false(secondCallback_happened);
       do_check_eq(tester.writtenData, firstData);
       firstCallback_happened = true;
     }, do_report_unexpected_exception);
@@ -235,35 +313,37 @@ function write_then_disable(aTester) {
   let length = aTester.writtenData.length;
   aTester.writeHandler = disabled_write_callback;
   do_execute_soon(() => aTester.waDeferred.resolve(length));
 }
 
 // Flush tests. First, do an ordinary clean save and then call flush;
 // there should not be another save
 add_task(function flush_after_save() {
-  let tester = DeferredSaveTester(1);
+  setQuickMockTimer();
+  let tester = DeferredSaveTester();
   let dataToSave = "Flush after save";
 
   yield tester.save(dataToSave);
   yield tester.flush(disabled_write_callback);
   do_check_eq(1, tester.saver.totalSaves);
 });
 
 // Flush while a write is in progress, but the in-memory data is clean
 add_task(function flush_during_write() {
-  let tester = DeferredSaveTester(1);
+  let tester = DeferredSaveTester();
   let dataToSave = "Flush during write";
   let firstCallback_happened = false;
   let writeStarted = Promise.defer();
 
   function writeCallback(aTester) {
     writeStarted.resolve(aTester.waDeferred);
   }
 
+  setQuickMockTimer();
   tester.save(dataToSave, writeCallback).then(
     count => {
       do_check_false(firstCallback_happened);
       firstCallback_happened = true;
     }, do_report_unexpected_exception);
 
   let writer = yield writeStarted.promise;
 
@@ -276,50 +356,55 @@ add_task(function flush_during_write() {
   yield flushing;
   do_check_true(firstCallback_happened);
   do_check_eq(1, tester.saver.totalSaves);
 });
 
 // Flush while dirty but write not in progress
 // The data written should be the value at the time
 // flush() is called, even if it is changed later
-//
-// It would be nice to have a mock for Timer in here, to control
-// when the steps happen, but for now we'll call the flush without
-// going back around the event loop to make sure it happens before
-// the DeferredSave timer goes off
 add_task(function flush_while_dirty() {
-  let tester = DeferredSaveTester(20);
+  let timerPromise = setPromiseMockTimer();
+  let tester = DeferredSaveTester();
   let firstData = "Flush while dirty, valid data";
   let firstCallback_happened = false;
 
   tester.save(firstData, write_then_disable).then(
     count => {
       do_check_false(firstCallback_happened);
       firstCallback_happened = true;
       do_check_eq(tester.writtenData, firstData);
     }, do_report_unexpected_exception);
 
+  // Wait for the timer to be set, but don't trigger it so the write won't start
+  let activeTimer = yield timerPromise;
+
+  let flushing = tester.flush();
+
+  // Make sure the timer was cancelled
+  do_check_true(activeTimer.isCancelled);
+
   // Also make sure that data changed after the flush call
   // (even without a saveChanges() call) doesn't get written
-  let flushing = tester.flush();
   tester.dataToSave = "Flush while dirty, invalid data";
+
   yield flushing;
   do_check_true(firstCallback_happened);
   do_check_eq(tester.writtenData, firstData);
   do_check_eq(1, tester.saver.totalSaves);
 });
 
 // And the grand finale - modify the data, start writing,
 // modify the data again so we're in progress and dirty,
 // then flush, then modify the data again
 // Data for the second write should be taken at the time
 // flush() is called, even if it is modified later
 add_task(function flush_writing_dirty() {
-  let tester = DeferredSaveTester(5);
+  let timerPromise = setPromiseMockTimer();
+  let tester = DeferredSaveTester();
   let firstData = "Flush first pass data";
   let secondData = "Flush second pass data";
   let firstCallback_happened = false;
   let secondCallback_happened = false;
   let writeStarted = Promise.defer();
 
   function writeCallback(aTester) {
     writeStarted.resolve(aTester.waDeferred);
@@ -327,32 +412,36 @@ add_task(function flush_writing_dirty() 
 
   tester.save(firstData, writeCallback).then(
     count => {
       do_check_false(firstCallback_happened);
       do_check_eq(tester.writtenData, firstData);
       firstCallback_happened = true;
     }, do_report_unexpected_exception);
 
+  // Trigger the timer callback as soon as the DeferredSave sets it
+  let activeTimer = yield timerPromise;
+  activeTimer.callback();
   let writer = yield writeStarted.promise;
   // the first write has started
 
   // dirty the data and request another save
   // after the second save completes, there should not be another write
   tester.save(secondData, write_then_disable).then(
     count => {
       do_check_true(firstCallback_happened);
       do_check_false(secondCallback_happened);
       do_check_eq(tester.writtenData, secondData);
       secondCallback_happened = true;
     }, do_report_unexpected_exception);
 
   let flushing = tester.flush(write_then_disable);
+  // Flush should have cancelled our timer
+  do_check_true(activeTimer.isCancelled);
   tester.dataToSave = "Flush, invalid data: changed late";
-  yield delay(1);
   // complete the first write
   writer.resolve(firstData.length);
   // now wait for the second write / flush to complete
   yield flushing;
   do_check_true(firstCallback_happened);
   do_check_true(secondCallback_happened);
   do_check_eq(tester.writtenData, secondData);
   do_check_eq(2, tester.saver.totalSaves);
@@ -370,38 +459,41 @@ function badDataProvider() {
   let err = new Error(badDataError);
   badDataError = "badDataProvider called twice";
   throw err;
 }
 
 // Handle cases where data provider throws
 // First, throws during a normal save
 add_task(function data_throw() {
+  setQuickMockTimer();
   badDataError = expectedDataError;
-  let tester = DeferredSaveTester(1, badDataProvider);
+  let tester = DeferredSaveTester(badDataProvider);
   yield tester.save("data_throw").then(
     count => do_throw("Expected serialization failure"),
     error => do_check_eq(error.message, expectedDataError));
 });
 
 // Now, throws during flush
 add_task(function data_throw_during_flush() {
   badDataError = expectedDataError;
-  let tester = DeferredSaveTester(1, badDataProvider);
+  let tester = DeferredSaveTester(badDataProvider);
   let firstCallback_happened = false;
 
+  setPromiseMockTimer();
   // Write callback should never be called
   tester.save("data_throw_during_flush", disabled_write_callback).then(
     count => do_throw("Expected serialization failure"),
     error => {
       do_check_false(firstCallback_happened);
       do_check_eq(error.message, expectedDataError);
       firstCallback_happened = true;
     });
 
+  // flush() will cancel the timer
   yield tester.flush(disabled_write_callback).then(
     count => do_throw("Expected serialization failure"),
     error => do_check_eq(error.message, expectedDataError)
     );
 
   do_check_true(firstCallback_happened);
 });
 
@@ -412,28 +504,30 @@ add_task(function data_throw_during_flus
 // finish writing (need to restart delayed timer)
 // saveChanges
 // flush
 // write starts
 // actually restart timer for delayed write
 // write completes
 // delayed timer goes off, throws error because DeferredSave has been torn down
 add_task(function delay_flush_race() {
-  let tester = DeferredSaveTester(5);
+  let timerPromise = setPromiseMockTimer();
+  let tester = DeferredSaveTester();
   let firstData = "First save";
   let secondData = "Second save";
   let thirdData = "Third save";
   let writeStarted = Promise.defer();
 
   function writeCallback(aTester) {
     writeStarted.resolve(aTester.waDeferred);
   }
 
   // This promise won't resolve until after writeStarted
   let firstSave = tester.save(firstData, writeCallback);
+  (yield timerPromise).callback();
 
   let writer = yield writeStarted.promise;
   // the first write has started
 
   // dirty the data and request another save
   let secondSave = tester.save(secondData);
 
   // complete the first write