Merge m-c to fig.
authorSriram Ramasubramanian <sriram@mozilla.com>
Fri, 09 Aug 2013 10:49:51 -0700
changeset 156424 d07b65be9f7a8d19552ea6443fc8dc3656da117a
parent 156423 fb365c5ad68dff263e75e7a7396e934738ae0efe (current diff)
parent 154759 e33c2011643e323aa29739ac67c28c588197cbb1 (diff)
child 156425 22d1104b4b0858fe8ae66db74b1bbf0371c0a524
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to fig.
CLOBBER
accessible/tests/mochitest/text/test_label.xul
browser/components/sessionstore/test/unit/test_no_backup_first_write.js
browser/devtools/shared/AutocompletePopup.jsm
browser/devtools/styleinspector/css-logic.js
browser/devtools/webconsole/HUDService.jsm
browser/devtools/webconsole/NetworkPanel.jsm
browser/devtools/webconsole/WebConsolePanel.jsm
browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
browser/metro/base/content/prompt/alert.xul
browser/metro/base/content/prompt/confirm.xul
browser/metro/base/content/prompt/crash.xul
browser/metro/base/content/prompt/prompt.js
browser/metro/base/content/prompt/prompt.xul
browser/metro/base/content/prompt/promptPassword.xul
browser/metro/base/content/prompt/select.xul
browser/metro/locales/en-US/chrome/crashprompt.dtd
browser/metro/locales/en-US/chrome/prompt.dtd
browser/modules/AboutHomeUtils.jsm
config/tests/unit-writemozinfo.py
config/writemozinfo.py
content/html/content/public/nsILink.h
dom/interfaces/html/nsIDOMHTMLHeadingElement.idl
dom/interfaces/html/nsIDOMHTMLLabelElement.idl
dom/interfaces/html/nsIDOMHTMLTableCellElement.idl
dom/interfaces/push/moz.build
dom/interfaces/push/nsIDOMPushManager.idl
dom/tests/mochitest/webapps/bug_765063.xul
dom/workers/ImageData.cpp
dom/workers/ImageData.h
media/webrtc/trunk/webrtc/modules/audio_device/android/audio_device_opensles_android.cc
media/webrtc/trunk/webrtc/modules/audio_device/android/audio_device_opensles_android.h
mobile/android/base/ActivityHandlerHelper.java
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/BrowserApp.java
mobile/android/base/BrowserToolbar.java
mobile/android/base/GeckoApp.java
mobile/android/base/Makefile.in
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/drawable-mdpi/progress_spinner_1.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_10.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_11.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_12.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_2.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_3.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_4.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_5.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_6.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_7.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_8.png
mobile/android/base/resources/drawable-mdpi/progress_spinner_9.png
mobile/android/base/resources/drawable/progress_spinner.xml
mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
mobile/android/base/resources/layout/browser_toolbar.xml
mobile/android/base/resources/values-v11/styles.xml
mobile/android/base/resources/values-v11/themes.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/strings.xml.in
mobile/android/base/tests/BaseTest.java.in
mobile/android/base/tests/testAddSearchEngine.java.in
mobile/android/base/tests/testWebContentContextMenu.java.in
mobile/android/chrome/content/browser.js
netwerk/base/public/nsIStrictTransportSecurityService.idl
security/manager/boot/src/nsStrictTransportSecurityService.cpp
security/manager/boot/src/nsStrictTransportSecurityService.h
testing/marionette/client/marionette/tests/unit/test_import_script_content.py
toolkit/devtools/server/tests/unit/test_trace_actor-07.js
toolkit/devtools/webconsole/NetworkHelper.jsm
toolkit/devtools/webconsole/WebConsoleClient.jsm
toolkit/devtools/webconsole/WebConsoleUtils.jsm
widget/gtk2/compat/gtk/gtkcolorselectiondialog.h
xpcom/typelib/xpidl/Makefile.in
xpcom/typelib/xpidl/moz.build
--- a/.hgtags
+++ b/.hgtags
@@ -89,8 +89,9 @@ 6fdf9985acfe6f939da584b2559464ab22264fe7
 fd72dbbd692012224145be1bf13df1d7675fd277 FIREFOX_AURORA_17_BASE
 2704e441363fe2a48e992dfac694482dfd82664a FIREFOX_AURORA_18_BASE
 cf8750abee06cde395c659f8ecd8ae019d7512e3 FIREFOX_AURORA_19_BASE
 5bb309998e7050c9ee80b0147de1e473f008e221 FIREFOX_AURORA_20_BASE
 cc37417e2c284aed960f98ffa479de4ccdd5c7c3 FIREFOX_AURORA_21_BASE
 1c070ab0f9db59f13423b9c1db60419f7a9098f9 FIREFOX_AURORA_22_BASE
 d7ce9089999719d5186595d160f25123a4e63e39 FIREFOX_AURORA_23_BASE
 8d3810543edccf4fbe458178b88dd4a6e420b010 FIREFOX_AURORA_24_BASE
+ad0ae007aa9e03cd74e9005cd6652e544139b3b5 FIREFOX_AURORA_25_BASE
--- a/CLOBBER
+++ b/CLOBBER
@@ -12,9 +12,10 @@
 #          O               O
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
-Bug 888905: Tablet bookmarks layout, makefile and xml changes requires clobbering.
+
+Add an WebIDL interface for bug 892978 requires a clobber for Windows.
--- a/accessible/src/base/nsCoreUtils.cpp
+++ b/accessible/src/base/nsCoreUtils.cpp
@@ -70,18 +70,17 @@ nsCoreUtils::DispatchClickEvent(nsITreeB
   if (!tcElm)
     return;
 
   nsCOMPtr<nsIContent> tcContent(do_QueryInterface(tcElm));
   nsIDocument *document = tcContent->GetCurrentDoc();
   if (!document)
     return;
 
-  nsIPresShell *presShell = nullptr;
-  presShell = document->GetShell();
+  nsCOMPtr<nsIPresShell> presShell = document->GetShell();
   if (!presShell)
     return;
 
   // Ensure row is visible.
   aTreeBoxObj->EnsureRowIsVisible(aRowIndex);
 
   // Calculate x and y coordinates.
   int32_t x = 0, y = 0, width = 0, height = 0;
@@ -97,24 +96,24 @@ nsCoreUtils::DispatchClickEvent(nsITreeB
 
   int32_t tcX = 0;
   tcBoxObj->GetX(&tcX);
 
   int32_t tcY = 0;
   tcBoxObj->GetY(&tcY);
 
   // Dispatch mouse events.
-  nsIFrame* tcFrame = tcContent->GetPrimaryFrame();
+  nsWeakFrame tcFrame = tcContent->GetPrimaryFrame();
   nsIFrame* rootFrame = presShell->GetRootFrame();
 
   nsPoint offset;
   nsIWidget *rootWidget =
     rootFrame->GetViewExternal()->GetNearestWidget(&offset);
 
-  nsPresContext* presContext = presShell->GetPresContext();
+  nsRefPtr<nsPresContext> presContext = presShell->GetPresContext();
 
   int32_t cnvdX = presContext->CSSPixelsToDevPixels(tcX + x + 1) +
     presContext->AppUnitsToDevPixels(offset.x);
   int32_t cnvdY = presContext->CSSPixelsToDevPixels(tcY + y + 1) +
     presContext->AppUnitsToDevPixels(offset.y);
 
   // XUL is just desktop, so there is no real reason for senfing touch events.
   DispatchMouseEvent(NS_MOUSE_BUTTON_DOWN, cnvdX, cnvdY,
--- a/accessible/src/generic/Accessible.cpp
+++ b/accessible/src/generic/Accessible.cpp
@@ -2280,38 +2280,37 @@ Accessible::DoCommand(nsIContent *aConte
 }
 
 void
 Accessible::DispatchClickEvent(nsIContent *aContent, uint32_t aActionIndex)
 {
   if (IsDefunct())
     return;
 
-  nsIPresShell* presShell = mDoc->PresShell();
+  nsCOMPtr<nsIPresShell> presShell = mDoc->PresShell();
 
   // Scroll into view.
   presShell->ScrollContentIntoView(aContent,
                                    nsIPresShell::ScrollAxis(),
                                    nsIPresShell::ScrollAxis(),
                                    nsIPresShell::SCROLL_OVERFLOW_HIDDEN);
 
-  nsIFrame* frame = aContent->GetPrimaryFrame();
+  nsWeakFrame frame = aContent->GetPrimaryFrame();
   if (!frame)
     return;
 
   // Compute x and y coordinates.
   nsPoint point;
   nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point);
   if (!widget)
     return;
 
   nsSize size = frame->GetSize();
 
-  nsPresContext* presContext = presShell->GetPresContext();
-
+  nsRefPtr<nsPresContext> presContext = presShell->GetPresContext();
   int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2);
   int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2);
 
   // Simulate a touch interaction by dispatching touch events with mouse events.
   nsCoreUtils::DispatchTouchEvent(NS_TOUCH_START, x, y, aContent, frame, presShell, widget);
   nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_DOWN, x, y, aContent, frame, presShell, widget);
   nsCoreUtils::DispatchTouchEvent(NS_TOUCH_END, x, y, aContent, frame, presShell, widget);
   nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_UP, x, y, aContent, frame, presShell, widget);
--- a/accessible/src/generic/BaseAccessibles.cpp
+++ b/accessible/src/generic/BaseAccessibles.cpp
@@ -9,17 +9,16 @@
 #include "HyperTextAccessibleWrap.h"
 #include "nsAccessibilityService.h"
 #include "nsAccUtils.h"
 #include "nsCoreUtils.h"
 #include "Role.h"
 #include "States.h"
 
 #include "nsGUIEvent.h"
-#include "nsILink.h"
 #include "nsINameSpaceManager.h"
 #include "nsIURI.h"
 
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
 // LeafAccessible
 ////////////////////////////////////////////////////////////////////////////////
--- a/accessible/src/generic/ImageAccessible.cpp
+++ b/accessible/src/generic/ImageAccessible.cpp
@@ -10,17 +10,16 @@
 #include "AccIterator.h"
 #include "States.h"
 
 #include "imgIContainer.h"
 #include "imgIRequest.h"
 #include "nsGenericHTMLElement.h"
 #include "nsIDocument.h"
 #include "nsIImageLoadingContent.h"
-#include "nsILink.h"
 #include "nsIPresShell.h"
 #include "nsIServiceManager.h"
 #include "nsIDOMHTMLImageElement.h"
 #include "nsPIDOMWindow.h"
 
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
--- a/accessible/src/xul/XULFormControlAccessible.cpp
+++ b/accessible/src/xul/XULFormControlAccessible.cpp
@@ -812,20 +812,27 @@ XULTextFieldAccessible::CacheChildren()
 {
   NS_ENSURE_TRUE_VOID(mDoc);
   // Create child accessibles for native anonymous content of underlying HTML
   // input element.
   nsCOMPtr<nsIContent> inputContent(GetInputField());
   if (!inputContent)
     return;
 
+  // XXX: entry shouldn't contain anything but text leafs. Currently it may
+  // contain a trailing fake HTML br element added for layout needs. We don't
+  // need to expose it since it'd be confusing for AT.
   TreeWalker walker(this, inputContent);
-
   Accessible* child = nullptr;
-  while ((child = walker.NextChild()) && AppendChild(child));
+  while ((child = walker.NextChild())) {
+    if (child->IsTextLeaf())
+      AppendChild(child);
+    else
+      Document()->UnbindFromDocument(child);
+  }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // XULTextFieldAccessible: HyperTextAccessible protected
 
 already_AddRefed<nsFrameSelection>
 XULTextFieldAccessible::FrameSelection()
 {
--- a/accessible/tests/mochitest/text/Makefile.in
+++ b/accessible/tests/mochitest/text/Makefile.in
@@ -11,19 +11,19 @@ relativesrcdir	= @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_A11Y_FILES = \
 		doc.html \
 		test_atcaretoffset.html \
 		test_charboundary.html \
 		test_doc.html \
+		test_general.xul \
 		test_gettext.html \
 		test_hypertext.html \
 		test_lineboundary.html \
-		test_label.xul \
 		test_passwords.html \
 		test_selection.html \
 		test_wordboundary.html \
 		test_words.html \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
rename from accessible/tests/mochitest/text/test_label.xul
rename to accessible/tests/mochitest/text/test_general.xul
--- a/accessible/tests/mochitest/text/test_label.xul
+++ b/accessible/tests/mochitest/text/test_general.xul
@@ -21,44 +21,60 @@
   <script type="application/javascript">
   <![CDATA[
     ////////////////////////////////////////////////////////////////////////////
     // Testing
 
     var gQueue = null;
     function doTests()
     {
+      //////////////////////////////////////////////////////////////////////////
+      // XUL label
+
       var ids = ["label1", "label2"];
 
       testCharacterCount(ids, 5);
 
       testText(ids, 0, -1, "Hello");
       testText(ids, 0, 1, "H");
 
       testCharAfterOffset(ids, 0, "e", 1, 2);
       testCharBeforeOffset(ids, 1, "H", 0, 1);
       testCharAtOffset(ids, 1, "e", 1, 2);
 
+      //////////////////////////////////////////////////////////////////////////
+      // XUL textbox
+
+      testTextAtOffset([ "tbox1" ], BOUNDARY_LINE_START,
+                       [ [ 0, 4, "test", 0, 4 ] ]);
+
       SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTests);
   ]]>
   </script>
 
   <vbox flex="1" style="overflow: auto;">
   <body xmlns="http://www.w3.org/1999/xhtml">
     <a target="_blank"
         href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166"
         title="xul:label@value accessible should implement nsIAccessibleText">
       Bug 396166
     </a>
+    <a target="_blank"
+        href="https://bugzilla.mozilla.org/show_bug.cgi?id=899433"
+        title="Accessibility returns empty line for last line in certain cases">
+      Bug 899433
+    </a>
     <p id="display"></p>
     <div id="content" style="display: none">
     </div>
     <pre id="test">
     </pre>
   </body>
   <label id="label1" value="Hello"/>
   <label id="label2">Hello</label>
+
+  <textbox id="tbox1" value="test" multiline="true"/>
   </vbox>
 </window>
--- a/accessible/tests/mochitest/tree/test_txtctrl.xul
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xul
@@ -67,20 +67,16 @@
       // multiline textbox
       ////////////////////
       accTree = {
         role: ROLE_ENTRY,
         children: [
           {
             role: ROLE_TEXT_LEAF,
             children: []
-          },
-          {
-            role: ROLE_WHITESPACE,
-            children: []
           }
         ]
       };
 
       testAccessibleTree("txc6", accTree);
 
       ////////////////////
       // autocomplete textbox
--- a/addon-sdk/source/test/test-plain-text-console.js
+++ b/addon-sdk/source/test/test-plain-text-console.js
@@ -31,25 +31,25 @@ exports.testPlainTextConsole = function(
   prefs.reset(ADDON_LOG_LEVEL_PREF);
 
   var Console = require("sdk/console/plain-text").PlainTextConsole;
   var con = new Console(print);
 
   test.pass("PlainTextConsole instantiates");
 
   con.log('testing', 1, [2, 3, 4]);
-  test.assertEqual(lastPrint(), "console.log: " + name + ": testing, 1, Array [2,3,4]\n",
+  test.assertEqual(lastPrint(), "console.log: " + name + ": testing 1 Array [2,3,4]\n",
                    "PlainTextConsole.log() must work.");
 
   con.info('testing', 1, [2, 3, 4]);
-  test.assertEqual(lastPrint(), "console.info: " + name + ": testing, 1, Array [2,3,4]\n",
+  test.assertEqual(lastPrint(), "console.info: " + name + ": testing 1 Array [2,3,4]\n",
                    "PlainTextConsole.info() must work.");
 
   con.warn('testing', 1, [2, 3, 4]);
-  test.assertEqual(lastPrint(), "console.warn: " + name + ": testing, 1, Array [2,3,4]\n",
+  test.assertEqual(lastPrint(), "console.warn: " + name + ": testing 1 Array [2,3,4]\n",
                    "PlainTextConsole.warn() must work.");
 
   con.error('testing', 1, [2, 3, 4]);
   test.assertEqual(prints[0], "console.error: " + name + ": \n",
                    "PlainTextConsole.error() must work.");
   test.assertEqual(prints[1], "  testing\n")
   test.assertEqual(prints[2], "  1\n")
   test.assertEqual(prints[3], "Array\n    - 0 = 2\n    - 1 = 3\n    - 2 = 4\n    - length = 3\n");
@@ -59,30 +59,30 @@ exports.testPlainTextConsole = function(
   test.assertEqual(prints[0], "console.debug: " + name + ": \n",
                    "PlainTextConsole.debug() must work.");
   test.assertEqual(prints[1], "  testing\n")
   test.assertEqual(prints[2], "  1\n")
   test.assertEqual(prints[3], "Array\n    - 0 = 2\n    - 1 = 3\n    - 2 = 4\n    - length = 3\n");
   prints = [];
 
   con.log('testing', undefined);
-  test.assertEqual(lastPrint(), "console.log: " + name + ": testing, undefined\n",
+  test.assertEqual(lastPrint(), "console.log: " + name + ": testing undefined\n",
                    "PlainTextConsole.log() must stringify undefined.");
 
   con.log('testing', null);
-  test.assertEqual(lastPrint(), "console.log: " + name + ": testing, null\n",
+  test.assertEqual(lastPrint(), "console.log: " + name + ": testing null\n",
                    "PlainTextConsole.log() must stringify null.");
 
   // TODO: Fix console.jsm to detect custom toString.
   con.log("testing", { toString: function() "obj.toString()" });
-  test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n",
+  test.assertEqual(lastPrint(), "console.log: " + name + ": testing {}\n",
                    "PlainTextConsole.log() doesn't printify custom toString.");
 
   con.log("testing", { toString: function() { throw "fail!"; } });
-  test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n",
+  test.assertEqual(lastPrint(), "console.log: " + name + ": testing {}\n",
                    "PlainTextConsole.log() must stringify custom bad toString.");
 
   
   con.exception(new Error("blah"));
 
   
   test.assertEqual(prints[0], "console.error: " + name + ": \n");
   let tbLines = prints[1].split("\n");
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -250,17 +250,16 @@ pref("layers.offmainthreadcomposition.en
 pref("layers.acceleration.disabled", false);
 pref("layers.offmainthreadcomposition.async-animations", true);
 pref("layers.async-video.enabled", true);
 pref("layers.async-pan-zoom.enabled", true);
 #endif
 
 // Web Notifications
 pref("notification.feature.enabled", true);
-pref("dom.webnotifications.enabled", false);
 
 // IndexedDB
 pref("indexedDB.feature.enabled", true);
 pref("dom.indexedDB.warningQuota", 5);
 
 // prevent video elements from preloading too much data
 pref("media.preload.default", 1); // default to preload none
 pref("media.preload.auto", 2);    // preload metadata if preload=auto
@@ -655,23 +654,23 @@ pref("dom.disable_window_open_dialog_fea
 pref("accessibility.accessfu.activate", 2);
 pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
 // Setting for an utterance order (0 - description first, 1 - description last).
 pref("accessibility.accessfu.utterance", 1);
 // Whether to skip images with empty alt text
 pref("accessibility.accessfu.skip_empty_images", true);
 
 // Enable hit-target fluffing
-pref("ui.touch.radius.enabled", false);
+pref("ui.touch.radius.enabled", true);
 pref("ui.touch.radius.leftmm", 3);
 pref("ui.touch.radius.topmm", 5);
 pref("ui.touch.radius.rightmm", 3);
 pref("ui.touch.radius.bottommm", 2);
 
-pref("ui.mouse.radius.enabled", false);
+pref("ui.mouse.radius.enabled", true);
 pref("ui.mouse.radius.leftmm", 3);
 pref("ui.mouse.radius.topmm", 5);
 pref("ui.mouse.radius.rightmm", 3);
 pref("ui.mouse.radius.bottommm", 2);
 
 // Disable native prompt
 pref("browser.prompt.allowNative", false);
 
--- a/b2g/chrome/content/forms.js
+++ b/b2g/chrome/content/forms.js
@@ -189,40 +189,47 @@ let FormAssistant = {
     addEventListener("input", this, true, false);
     addEventListener("keydown", this, true, false);
     addEventListener("keyup", this, true, false);
     addMessageListener("Forms:Select:Choice", this);
     addMessageListener("Forms:Input:Value", this);
     addMessageListener("Forms:Select:Blur", this);
     addMessageListener("Forms:SetSelectionRange", this);
     addMessageListener("Forms:ReplaceSurroundingText", this);
+    addMessageListener("Forms:GetText", this);
+    addMessageListener("Forms:Input:SendKey", this);
+    addMessageListener("Forms:GetContext", this);
   },
 
   ignoredInputTypes: new Set([
     'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image'
   ]),
 
   isKeyboardOpened: false,
   selectionStart: -1,
   selectionEnd: -1,
+  textBeforeCursor: "",
+  textAfterCursor: "",
   scrollIntoViewTimeout: null,
   _focusedElement: null,
+  _focusCounter: 0, // up one for every time we focus a new element
   _documentEncoder: null,
   _editor: null,
   _editing: false,
   _ignoreEditActionOnce: false,
 
   get focusedElement() {
     if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
       this._focusedElement = null;
 
     return this._focusedElement;
   },
 
   set focusedElement(val) {
+    this._focusCounter++;
     this._focusedElement = val;
   },
 
   setFocusedElement: function fa_setFocusedElement(element) {
     if (element === this.focusedElement)
       return;
 
     if (this.focusedElement) {
@@ -385,32 +392,67 @@ let FormAssistant = {
       case "keyup":
         this._ignoreEditActionOnce = false;
         break;
     }
   },
 
   receiveMessage: function fa_receiveMessage(msg) {
     let target = this.focusedElement;
+    let json = msg.json;
+
+    // To not break mozKeyboard contextId is optional
+    if ('contextId' in json &&
+        json.contextId !== this._focusCounter &&
+        json.requestId) {
+      // Ignore messages that are meant for a previously focused element
+      sendAsyncMessage("Forms:SequenceError", {
+        requestId: json.requestId,
+        error: "Expected contextId " + this._focusCounter +
+               " but was " + json.contextId
+      });
+      return;
+    }
+
     if (!target) {
+      switch (msg.name) {
+      case "Forms:GetText":
+        sendAsyncMessage("Forms:GetText:Result:Error", {
+          requestId: json.requestId,
+          error: "No focused element"
+        });
+        break;
+      }
       return;
     }
 
     this._editing = true;
-    let json = msg.json;
     switch (msg.name) {
       case "Forms:Input:Value": {
         target.value = json.value;
 
         let event = target.ownerDocument.createEvent('HTMLEvents');
         event.initEvent('input', true, false);
         target.dispatchEvent(event);
         break;
       }
 
+      case "Forms:Input:SendKey":
+        ["keydown", "keypress", "keyup"].forEach(function(type) {
+          domWindowUtils.sendKeyEvent(type, json.keyCode, json.charCode,
+            json.modifiers);
+        });
+
+        if (json.requestId) {
+          sendAsyncMessage("Forms:SendKey:Result:OK", {
+            requestId: json.requestId
+          });
+        }
+        break;
+
       case "Forms:Select:Choice":
         let options = target.options;
         let valueChanged = false;
         if ("index" in json) {
           if (options.selectedIndex != json.index) {
             options.selectedIndex = json.index;
             valueChanged = true;
           }
@@ -437,25 +479,68 @@ let FormAssistant = {
         break;
       }
 
       case "Forms:SetSelectionRange":  {
         let start = json.selectionStart;
         let end =  json.selectionEnd;
         setSelectionRange(target, start, end);
         this.updateSelection();
+
+        if (json.requestId) {
+          sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
+            requestId: json.requestId,
+            selectioninfo: this.getSelectionInfo()
+          });
+        }
         break;
       }
 
       case "Forms:ReplaceSurroundingText": {
         let text = json.text;
         let beforeLength = json.beforeLength;
         let afterLength = json.afterLength;
-        replaceSurroundingText(target, text, this.selectionStart, beforeLength,
+        let selectionRange = getSelectionRange(target);
+
+        replaceSurroundingText(target, text, selectionRange[0], beforeLength,
                                afterLength);
+
+        if (json.requestId) {
+          sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
+            requestId: json.requestId,
+            selectioninfo: this.getSelectionInfo()
+          });
+        }
+        break;
+      }
+
+      case "Forms:GetText": {
+        let isPlainTextField = target instanceof HTMLInputElement ||
+                               target instanceof HTMLTextAreaElement;
+        let value = isPlainTextField ?
+          target.value :
+          getContentEditableText(target);
+
+        if (json.offset && json.length) {
+          value = value.substr(json.offset, json.length);
+        }
+        else if (json.offset) {
+          value = value.substr(json.offset);
+        }
+
+        sendAsyncMessage("Forms:GetText:Result:OK", {
+          requestId: json.requestId,
+          text: value
+        });
+        break;
+      }
+
+      case "Forms:GetContext": {
+        let obj = getJSON(target, this._focusCounter);
+        sendAsyncMessage("Forms:GetContext:Result:OK", obj);
         break;
       }
     }
     this._editing = false;
 
   },
 
   showKeyboard: function fa_showKeyboard(target) {
@@ -524,30 +609,57 @@ let FormAssistant = {
   sendKeyboardState: function(element) {
     // FIXME/bug 729623: work around apparent bug in the IME manager
     // in gecko.
     let readonly = element.getAttribute("readonly");
     if (readonly) {
       return false;
     }
 
-    sendAsyncMessage("Forms:Input", getJSON(element));
+    sendAsyncMessage("Forms:Input", getJSON(element, this._focusCounter));
     return true;
   },
 
+  getSelectionInfo: function fa_getSelectionInfo() {
+    let element = this.focusedElement;
+    let range =  getSelectionRange(element);
+
+    let isPlainTextField = element instanceof HTMLInputElement ||
+                           element instanceof HTMLTextAreaElement;
+
+    let text = isPlainTextField ?
+      element.value :
+      getContentEditableText(element);
+
+    let textAround = getTextAroundCursor(text, range);
+
+    let changed = this.selectionStart !== range[0] ||
+      this.selectionEnd !== range[1] ||
+      this.textBeforeCursor !== textAround.before ||
+      this.textAfterCursor !== textAround.after;
+
+    this.selectionStart = range[0];
+    this.selectionEnd = range[1];
+    this.textBeforeCursor = textAround.before;
+    this.textAfterCursor = textAround.after;
+
+    return {
+      selectionStart: range[0],
+      selectionEnd: range[1],
+      textBeforeCursor: textAround.before,
+      textAfterCursor: textAround.after,
+      changed: changed
+    };
+  },
+
   // Notify when the selection range changes
   updateSelection: function fa_updateSelection() {
-    let range =  getSelectionRange(this.focusedElement);
-    if (range[0] != this.selectionStart || range[1] != this.selectionEnd) {
-      this.selectionStart = range[0];
-      this.selectionEnd = range[1];
-      sendAsyncMessage("Forms:SelectionChange", {
-        selectionStart: range[0],
-        selectionEnd: range[1]
-      });
+    let selectionInfo = this.getSelectionInfo();
+    if (selectionInfo.changed) {
+      sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
     }
   }
 };
 
 FormAssistant.init();
 
 function isContentEditable(element) {
   if (!element) {
@@ -563,17 +675,17 @@ function isContentEditable(element) {
       element.contentDocument &&
       (element.contentDocument.body.isContentEditable ||
        element.contentDocument.designMode == "on"))
     return true;
 
   return element.ownerDocument && element.ownerDocument.designMode == "on";
 }
 
-function getJSON(element) {
+function getJSON(element, focusCounter) {
   let type = element.type || "";
   let value = element.value || "";
   let max = element.max || "";
   let min = element.min || "";
 
   // Treat contenteditble element as a special text area field
   if (isContentEditable(element)) {
     type = "textarea";
@@ -604,26 +716,47 @@ function getJSON(element) {
   let inputmode = element.getAttribute('x-inputmode');
   if (inputmode) {
     inputmode = inputmode.toLowerCase();
   } else {
     inputmode = '';
   }
 
   let range = getSelectionRange(element);
+  let textAround = getTextAroundCursor(value, range);
 
   return {
+    "contextId": focusCounter,
+
     "type": type.toLowerCase(),
     "choices": getListForElement(element),
     "value": value,
     "inputmode": inputmode,
     "selectionStart": range[0],
     "selectionEnd": range[1],
     "max": max,
-    "min": min
+    "min": min,
+    "lang": element.lang || "",
+    "textBeforeCursor": textAround.before,
+    "textAfterCursor": textAround.after
+  };
+}
+
+function getTextAroundCursor(value, range) {
+  let textBeforeCursor = range[0] < 100 ?
+    value.substr(0, range[0]) :
+    value.substr(range[0] - 100, 100);
+
+  let textAfterCursor = range[1] + 100 > value.length ?
+    value.substr(range[0], value.length) :
+    value.substr(range[0], range[1] - range[0] + 100);
+
+  return {
+    before: textBeforeCursor,
+    after: textAfterCursor
   };
 }
 
 function getListForElement(element) {
   if (!(element instanceof HTMLSelectElement))
     return null;
 
   let optionIndex = 0;
--- a/b2g/components/B2GComponents.manifest
+++ b/b2g/components/B2GComponents.manifest
@@ -15,18 +15,18 @@ component {88b3eb21-d072-4e3b-886d-f89d8
 contract @mozilla.org/updates/update-prompt;1 {88b3eb21-d072-4e3b-886d-f89d8c49fe59}
 #endif
 
 # MozKeyboard.js
 component {397a7fdf-2254-47be-b74e-76625a1a66d5} MozKeyboard.js
 contract @mozilla.org/b2g-keyboard;1 {397a7fdf-2254-47be-b74e-76625a1a66d5}
 category JavaScript-navigator-property mozKeyboard @mozilla.org/b2g-keyboard;1
 
-component {5c7f4ce1-a946-4adc-89e6-c908226341a0} MozKeyboard.js
-contract @mozilla.org/b2g-inputmethod;1 {5c7f4ce1-a946-4adc-89e6-c908226341a0}
+component {4607330d-e7d2-40a4-9eb8-43967eae0142} MozKeyboard.js
+contract @mozilla.org/b2g-inputmethod;1 {4607330d-e7d2-40a4-9eb8-43967eae0142}
 category JavaScript-navigator-property mozInputMethod @mozilla.org/b2g-inputmethod;1
 
 # DirectoryProvider.js
 component {9181eb7c-6f87-11e1-90b1-4f59d80dd2e5} DirectoryProvider.js
 contract @mozilla.org/browser/directory-provider;1 {9181eb7c-6f87-11e1-90b1-4f59d80dd2e5}
 category xpcom-directory-providers browser-directory-provider @mozilla.org/browser/directory-provider;1
 
 # ActivitiesGlue.js
--- a/b2g/components/Keyboard.jsm
+++ b/b2g/components/Keyboard.jsm
@@ -17,17 +17,18 @@ Cu.import("resource://gre/modules/XPCOMU
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
   "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
 
 let Keyboard = {
   _messageManager: null,
   _messageNames: [
     'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
     'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
-    'SwitchToNextInputMethod', 'HideInputMethod'
+    'SwitchToNextInputMethod', 'HideInputMethod',
+    'GetText', 'SendKey', 'GetContext'
   ],
 
   get messageManager() {
     if (this._messageManager && !Cu.isDeadWrapper(this._messageManager))
       return this._messageManager;
 
     throw Error('no message manager set');
   },
@@ -53,16 +54,23 @@ let Keyboard = {
       if (this.messageManager == mm) {
         // The application has been closed unexpectingly. Let's tell the
         // keyboard app that the focus has been lost.
         ppmm.broadcastAsyncMessage('Keyboard:FocusChange', { 'type': 'blur' });
       }
     } else {
       mm.addMessageListener('Forms:Input', this);
       mm.addMessageListener('Forms:SelectionChange', this);
+      mm.addMessageListener('Forms:GetText:Result:OK', this);
+      mm.addMessageListener('Forms:GetText:Result:Error', this);
+      mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
+      mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
+      mm.addMessageListener('Forms:SendKey:Result:OK', this);
+      mm.addMessageListener('Forms:SequenceError', this);
+      mm.addMessageListener('Forms:GetContext:Result:OK', this);
 
       // When not running apps OOP, we need to load forms.js here since this
       // won't happen from dom/ipc/preload.js
       try {
          if (Services.prefs.getBoolPref("dom.ipc.tabs.disabled") === true) {
            mm.loadFrameScript(kFormsFrameScript, true);
         }
       } catch (e) {
@@ -93,21 +101,30 @@ let Keyboard = {
         dump("Keyboard message " + msg.name +
         " from a content process with no 'keyboard' privileges.");
         return;
       }
     }
 
     switch (msg.name) {
       case 'Forms:Input':
-        this.handleFormsInput(msg);
+        this.forwardEvent('Keyboard:FocusChange', msg);
         break;
       case 'Forms:SelectionChange':
-        this.handleFormsSelectionChange(msg);
+      case 'Forms:GetText:Result:OK':
+      case 'Forms:GetText:Result:Error':
+      case 'Forms:SetSelectionRange:Result:OK':
+      case 'Forms:ReplaceSurroundingText:Result:OK':
+      case 'Forms:SendKey:Result:OK':
+      case 'Forms:SequenceError':
+      case 'Forms:GetContext:Result:OK':
+        let name = msg.name.replace(/^Forms/, 'Keyboard');
+        this.forwardEvent(name, msg);
         break;
+
       case 'Keyboard:SetValue':
         this.setValue(msg);
         break;
       case 'Keyboard:RemoveFocus':
         this.removeFocus();
         break;
       case 'Keyboard:SetSelectedOption':
         this.setSelectedOption(msg);
@@ -122,31 +139,33 @@ let Keyboard = {
         this.replaceSurroundingText(msg);
         break;
       case 'Keyboard:SwitchToNextInputMethod':
         this.switchToNextInputMethod();
         break;
       case 'Keyboard:ShowInputMethodPicker':
         this.showInputMethodPicker();
         break;
+      case 'Keyboard:GetText':
+        this.getText(msg);
+        break;
+      case 'Keyboard:SendKey':
+        this.sendKey(msg);
+        break;
+      case 'Keyboard:GetContext':
+        this.getContext(msg);
+        break;
     }
   },
 
-  handleFormsInput: function keyboardHandleFormsInput(msg) {
+  forwardEvent: function keyboardForwardEvent(newEventName, msg) {
     this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
                              .frameLoader.messageManager;
 
-    ppmm.broadcastAsyncMessage('Keyboard:FocusChange', msg.data);
-  },
-
-  handleFormsSelectionChange: function keyboardHandleFormsSelectionChange(msg) {
-    this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
-                             .frameLoader.messageManager;
-
-    ppmm.broadcastAsyncMessage('Keyboard:SelectionChange', msg.data);
+    ppmm.broadcastAsyncMessage(newEventName, msg.data);
   },
 
   setSelectedOption: function keyboardSetSelectedOption(msg) {
     this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
   },
 
   setSelectedOptions: function keyboardSetSelectedOptions(msg) {
     this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
@@ -176,12 +195,24 @@ let Keyboard = {
     });
   },
 
   switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
     let browser = Services.wm.getMostRecentWindow("navigator:browser");
     browser.shell.sendChromeEvent({
       type: "input-method-switch-to-next"
     });
+  },
+
+  getText: function keyboardGetText(msg) {
+    this.messageManager.sendAsyncMessage('Forms:GetText', msg.data);
+  },
+
+  sendKey: function keyboardSendKey(msg) {
+    this.messageManager.sendAsyncMessage('Forms:Input:SendKey', msg.data);
+  },
+
+  getContext: function keyboardGetContext(msg) {
+    this.messageManager.sendAsyncMessage('Forms:GetContext', msg.data);
   }
 };
 
 Keyboard.init();
--- a/b2g/components/MozKeyboard.js
+++ b/b2g/components/MozKeyboard.js
@@ -5,16 +5,17 @@
 "use strict";
 
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ObjectWrapper.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
   "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender");
 
 XPCOMUtils.defineLazyServiceGetter(this, "tm",
   "@mozilla.org/thread-manager;1", "nsIThreadManager");
 
 // -----------------------------------------------------------------------
@@ -239,43 +240,409 @@ MozInputMethodManager.prototype = {
 /**
  * ==============================================
  * InputMethod
  * ==============================================
  */
 function MozInputMethod() { }
 
 MozInputMethod.prototype = {
-  classID: Components.ID("{5c7f4ce1-a946-4adc-89e6-c908226341a0}"),
+  _inputcontext: null,
+
+  classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIInputMethod,
-    Ci.nsIDOMGlobalPropertyInitializer
+    Ci.nsIDOMGlobalPropertyInitializer,
+    Ci.nsIObserver
   ]),
 
   classInfo: XPCOMUtils.generateCI({
-    "classID": Components.ID("{5c7f4ce1-a946-4adc-89e6-c908226341a0}"),
+    "classID": Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
     "contractID": "@mozilla.org/b2g-inputmethod;1",
     "interfaces": [Ci.nsIInputMethod],
     "flags": Ci.nsIClassInfo.DOM_OBJECT,
     "classDescription": "B2G Input Method"
   }),
 
   init: function mozInputMethodInit(win) {
     let principal = win.document.nodePrincipal;
     let perm = Services.perms
                .testExactPermissionFromPrincipal(principal, "keyboard");
     if (perm != Ci.nsIPermissionManager.ALLOW_ACTION) {
       dump("No permission to use the keyboard API for " +
            principal.origin + "\n");
       return null;
     }
 
+    this._window = win;
     this._mgmt = new MozInputMethodManager();
+    this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIDOMWindowUtils)
+                            .currentInnerWindowID;
+
+    Services.obs.addObserver(this, "inner-window-destroyed", false);
+    cpmm.addMessageListener('Keyboard:FocusChange', this);
+    cpmm.addMessageListener('Keyboard:SelectionChange', this);
+    cpmm.addMessageListener('Keyboard:GetContext:Result:OK', this);
+
+    // If there already is an active context, then this will trigger
+    // a GetContext:Result:OK event, and we can initialize ourselves.
+    // Otherwise silently ignored.
+    cpmm.sendAsyncMessage("Keyboard:GetContext", {});
+  },
+
+  uninit: function mozInputMethodUninit() {
+    Services.obs.removeObserver(this, "inner-window-destroyed");
+    cpmm.removeMessageListener('Keyboard:FocusChange', this);
+    cpmm.removeMessageListener('Keyboard:SelectionChange', this);
+    cpmm.removeMessageListener('Keyboard:GetContext:Result:OK', this);
+
+    this._window = null;
+    this._inputcontextHandler = null;
+    this._mgmt = null;
+  },
+
+  receiveMessage: function mozInputMethodReceiveMsg(msg) {
+    let json = msg.json;
+
+    switch(msg.name) {
+      case 'Keyboard:FocusChange':
+        if (json.type !== 'blur') {
+          this.setInputContext(json);
+        }
+        else {
+          this.setInputContext(null);
+        }
+        break;
+      case 'Keyboard:SelectionChange':
+        this._inputcontext.updateSelectionContext(json);
+        break;
+      case 'Keyboard:GetContext:Result:OK':
+        this.setInputContext(json);
+        break;
+    }
+  },
+
+  observe: function mozInputMethodObserve(subject, topic, data) {
+    let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    if (wId == this.innerWindowID)
+      this.uninit();
   },
 
   get mgmt() {
     return this._mgmt;
+  },
+
+  get inputcontext() {
+     return this._inputcontext;
+  },
+
+  set oninputcontextchange(val) {
+    this._inputcontextHandler = val;
+  },
+
+  get oninputcontextchange() {
+    return this._inputcontextHandler;
+  },
+
+  setInputContext: function mozKeyboardContextChange(data) {
+    if (this._inputcontext) {
+      this._inputcontext.destroy();
+      this._inputcontext = null;
+    }
+
+    if (data) {
+      this._inputcontext = new MozInputContext(data);
+      this._inputcontext.init(this._window);
+    }
+
+    let handler = this._inputcontextHandler;
+    if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
+      return;
+
+    let evt = new this._window.CustomEvent("inputcontextchange",
+        ObjectWrapper.wrap({}, this._window));
+    handler.handleEvent(evt);
+  }
+};
+
+ /**
+ * ==============================================
+ * InputContext
+ * ==============================================
+ */
+function MozInputContext(ctx) {
+  this._context = {
+    inputtype: ctx.type,
+    inputmode: ctx.inputmode,
+    lang: ctx.lang,
+    type: ["textarea", "contenteditable"].indexOf(ctx.type) > -1 ?
+              ctx.type :
+              "text",
+    selectionStart: ctx.selectionStart,
+    selectionEnd: ctx.selectionEnd,
+    textBeforeCursor: ctx.textBeforeCursor,
+    textAfterCursor: ctx.textAfterCursor
+  };
+
+  this._contextId = ctx.contextId;
+}
+
+MozInputContext.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  _context: null,
+  _contextId: -1,
+
+  classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIB2GInputContext,
+    Ci.nsIObserver
+  ]),
+
+  classInfo: XPCOMUtils.generateCI({
+    "classID": Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
+    "contractID": "@mozilla.org/b2g-inputcontext;1",
+    "interfaces": [Ci.nsIB2GInputContext],
+    "flags": Ci.nsIClassInfo.DOM_OBJECT,
+    "classDescription": "B2G Input Context"
+  }),
+
+  init: function ic_init(win) {
+    this._window = win;
+    this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIDOMWindowUtils);
+
+    this.initDOMRequestHelper(win,
+      ["Keyboard:GetText:Result:OK",
+       "Keyboard:GetText:Result:Error",
+       "Keyboard:SetSelectionRange:Result:OK",
+       "Keyboard:ReplaceSurroundingText:Result:OK",
+       "Keyboard:SendKey:Result:OK",
+       "Keyboard:SequenceError"]);
+  },
+
+  destroy: function ic_destroy() {
+    let self = this;
+
+    // All requests that are still pending need to be invalidated
+    // because the context is no longer valid.
+    Object.keys(self._requests).forEach(function(k) {
+      // takeRequest also does a delete from context
+      let req = self.takeRequest(k);
+      Services.DOMRequest.fireError(req, "InputContext got destroyed");
+    });
+
+    this.destroyDOMRequestHelper();
+
+    // A consuming application might still hold a cached version of this
+    // object. After destroying the DOMRequestHelper all methods will throw
+    // because we cannot create new requests anymore, but we still hold
+    // (outdated) information in the context. So let's clear that out.
+    for (var k in this._context)
+      if (this._context.hasOwnProperty(k))
+        this._context[k] = null;
+  },
+
+  receiveMessage: function ic_receiveMessage(msg) {
+    if (!msg || !msg.json) {
+      dump('InputContext received message without data\n');
+      return;
+    }
+
+    let json = msg.json;
+    let request = json.requestId ? this.takeRequest(json.requestId) : null;
+
+    if (!request) {
+      return;
+    }
+
+    switch (msg.name) {
+      case "Keyboard:SendKey:Result:OK":
+        Services.DOMRequest.fireSuccess(request, null);
+        break;
+      case "Keyboard:GetText:Result:OK":
+        Services.DOMRequest.fireSuccess(request, json.text);
+        break;
+      case "Keyboard:GetText:Result:Error":
+        Services.DOMRequest.fireError(request, json.error);
+        break;
+      case "Keyboard:SetSelectionRange:Result:OK":
+      case "Keyboard:ReplaceSurroundingText:Result:OK":
+        Services.DOMRequest.fireSuccess(request,
+          ObjectWrapper.wrap(json.selectioninfo, this._window));
+        break;
+      case "Keyboard:SequenceError":
+        // Occurs when a new element got focus, but the inputContext was
+        // not invalidated yet...
+        Services.DOMRequest.fireError(request, "InputContext has expired");
+        break;
+      default:
+        Services.DOMRequest.fireError(request, "Could not find a handler for " +
+          msg.name);
+        break;
+    }
+  },
+
+  updateSelectionContext: function ic_updateSelectionContext(ctx) {
+    if (!this._context) {
+      return;
+    }
+
+    let selectionDirty = this._context.selectionStart !== ctx.selectionStart ||
+          this._context.selectionEnd !== ctx.selectionEnd;
+    let surroundDirty = this._context.textBeforeCursor !== ctx.textBeforeCursor ||
+          this._context.textAfterCursor !== ctx.textAfterCursor;
+
+    this._context.selectionStart = ctx.selectionStart;
+    this._context.selectionEnd = ctx.selectionEnd;
+    this._context.textBeforeCursor = ctx.textBeforeCursor;
+    this._context.textAfterCursor = ctx.textAfterCursor;
+
+    if (selectionDirty) {
+      this._fireEvent(this._onselectionchange, "selectionchange", {
+        selectionStart: ctx.selectionStart,
+        selectionEnd: ctx.selectionEnd
+      });
+    }
+
+    if (surroundDirty) {
+      this._fireEvent(this._onsurroundingtextchange, "surroundingtextchange", {
+        beforeString: ctx.textBeforeCursor,
+        afterString: ctx.textAfterCursor
+      });
+    }
+  },
+
+  _fireEvent: function ic_fireEvent(handler, eventName, aDetail) {
+    if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
+      return;
+
+    let detail = {
+      detail: aDetail
+    };
+
+    let evt = new this._window.CustomEvent(eventName,
+        ObjectWrapper.wrap(aDetail, this._window));
+    handler.handleEvent(evt);
+  },
+
+  // tag name of the input field
+  get type() {
+    return this._context.type;
+  },
+
+  // type of the input field
+  get inputType() {
+    return this._context.inputtype;
+  },
+
+  get inputMode() {
+    return this._context.inputmode;
+  },
+
+  get lang() {
+    return this._context.lang;
+  },
+
+  getText: function ic_getText(offset, length) {
+    let request = this.createRequest();
+
+    cpmm.sendAsyncMessage('Keyboard:GetText', {
+      contextId: this._contextId,
+      requestId: this.getRequestId(request),
+      offset: offset,
+      length: length
+    });
+
+    return request;
+  },
+
+  get selectionStart() {
+    return this._context.selectionStart;
+  },
+
+  get selectionEnd() {
+    return this._context.selectionEnd;
+  },
+
+  get textBeforeCursor() {
+    return this._context.textBeforeCursor;
+  },
+
+  get textAfterCursor() {
+    return this._context.textAfterCursor;
+  },
+
+  setSelectionRange: function ic_setSelectionRange(start, length) {
+    let request = this.createRequest();
+
+    cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", {
+      contextId: this._contextId,
+      requestId: this.getRequestId(request),
+      selectionStart: start,
+      selectionEnd: start + length
+    });
+
+    return request;
+  },
+
+  get onsurroundingtextchange() {
+    return this._onsurroundingtextchange;
+  },
+
+  set onsurroundingtextchange(handler) {
+    this._onsurroundingtextchange = handler;
+  },
+
+  get onselectionchange() {
+    return this._onselectionchange;
+  },
+
+  set onselectionchange(handler) {
+    this._onselectionchange = handler;
+  },
+
+  replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
+    let request = this.createRequest();
+
+    cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', {
+      contextId: this._contextId,
+      requestId: this.getRequestId(request),
+      text: text,
+      beforeLength: offset || 0,
+      afterLength: length || 0
+    });
+
+    return request;
+  },
+
+  deleteSurroundingText: function ic_deleteSurrText(offset, length) {
+    return this.replaceSurroundingText(null, offset, length);
+  },
+
+  sendKey: function ic_sendKey(keyCode, charCode, modifiers) {
+    let request = this.createRequest();
+
+    cpmm.sendAsyncMessage('Keyboard:SendKey', {
+      contextId: this._contextId,
+      requestId: this.getRequestId(request),
+      keyCode: keyCode,
+      charCode: charCode,
+      modifiers: modifiers
+    });
+
+    return request;
+  },
+
+  setComposition: function ic_setComposition(text, cursor) {
+    throw "Not implemented";
+  },
+
+  endComposition: function ic_endComposition(text) {
+    throw "Not implemented";
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
   [MozKeyboard, MozInputMethodManager, MozInputMethod]);
--- a/b2g/components/b2g.idl
+++ b/b2g/components/b2g.idl
@@ -1,21 +1,136 @@
 /* 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/. */
 
 #include "domstubs.idl"
 
+interface nsIDOMDOMRequest;
+
 [scriptable, uuid(3615a616-571d-4194-bf54-ccf546067b14)]
 interface nsIB2GCameraContent : nsISupports
 {
   /* temporary solution, waiting for getUserMedia */
   DOMString getCameraURI([optional] in jsval options);
 };
 
+[scriptable, uuid(1e38633d-d08b-4867-9944-afa5c648adb6)]
+interface nsIB2GInputContext : nsISupports
+{
+  // The tag name of input field, which is enum of "input", "textarea", or "contenteditable"
+  readonly attribute DOMString type;
+
+  // The type of the input field, which is enum of text, number, password, url, search, email, and so on.
+  // See http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#states-of-the-type-attribute
+  readonly attribute DOMString inputType;
+
+  /*
+   * The inputmode string, representing the input mode.
+   * See http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#input-modalities:-the-inputmode-attribute
+   */
+  readonly attribute DOMString inputMode;
+
+  /*
+   * The primary language for the input field.
+   * It is the value of HTMLElement.lang.
+   * See http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#htmlelement
+   */
+  readonly attribute DOMString lang;
+
+  /*
+   * Get the whole text content of the input field.
+   */
+  nsIDOMDOMRequest getText([optional] in long offset, [optional] in long length);
+
+  // The start and stop position of the selection.
+  readonly attribute long selectionStart;
+  readonly attribute long selectionEnd;
+
+  // The start and stop position of the selection.
+  readonly attribute DOMString textBeforeCursor;
+  readonly attribute DOMString textAfterCursor;
+
+  /*
+   * Set the selection range of the the editable text.
+   * Note: This method cannot be used to move the cursor during composition. Calling this
+   * method will cancel composition.
+   * @param start The beginning of the selected text.
+   * @param length The length of the selected text.
+   *
+   * Note that the start position should be less or equal to the end position.
+   * To move the cursor, set the start and end position to the same value.
+   */
+  nsIDOMDOMRequest setSelectionRange(in long start, in long length);
+
+  /*
+   * Commit text to current input field and replace text around cursor position. It will clear the current composition.
+   *
+   * @param text The string to be replaced with.
+   * @param offset The offset from the cursor position where replacing starts. Defaults to 0.
+   * @param length The length of text to replace. Defaults to 0.
+   */
+  nsIDOMDOMRequest replaceSurroundingText(in DOMString text, [optional] in long offset, [optional] in long length);
+
+  /*
+   *
+   * Delete text around the cursor.
+   * @param offset The offset from the cursor position where deletion starts.
+   * @param length The length of text to delete.
+   * TODO: maybe updateSurroundingText(DOMString beforeText, DOMString afterText); ?
+   */
+  nsIDOMDOMRequest deleteSurroundingText(in long offset, in long length);
+
+  /*
+   * Notifies when the text around the cursor is changed, due to either text
+   * editing or cursor movement. If the cursor has been moved, but the text around has not
+   * changed, the IME won't get notification.
+   *
+   * The event handler function is specified as:
+   * @param beforeString Text before and including cursor position.
+   * @param afterString Text after and excluing cursor position.
+   * function(DOMString beforeText, DOMString afterText) {
+   * ...
+   *  }
+   */
+  attribute nsIDOMEventListener onsurroundingtextchange;
+
+  attribute nsIDOMEventListener onselectionchange;
+
+  /*
+   * send a character with its key events.
+   * @param modifiers see http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl#206
+   * @return true if succeeds. Otherwise false if the input context becomes void.
+   * Alternative: sendKey(KeyboardEvent event), but we will likely waste memory for creating the KeyboardEvent object.
+   */
+  nsIDOMDOMRequest sendKey(in long keyCode, in long charCode, in long modifiers);
+
+  /*
+   * Set current composition. It will start or update composition.
+   * @param cursor Position in the text of the cursor.
+   *
+   * The API implementation should automatically ends the composition
+   * session (with event and confirm the current composition) if
+   * endComposition is never called. Same apply when the inputContext is lost
+   * during a unfinished composition session.
+   */
+  nsIDOMDOMRequest setComposition(in DOMString text, in long cursor);
+
+  /*
+   * End composition and actually commit the text. (was |commitText(text, offset, length)|)
+   * Ending the composition with an empty string will not send any text.
+   * Note that if composition always ends automatically (with the current composition committed) if the composition
+   * did not explicitly with |endComposition()| but was interrupted with |sendKey()|, |setSelectionRange()|,
+   * user moving the cursor, or remove the focus, etc.
+   *
+   * @param text The text
+   */
+  nsIDOMDOMRequest endComposition(in DOMString text);
+};
+
 [scriptable, uuid(40ad96b2-9efa-41fb-84c7-fbcec9b153f0)]
 interface nsIB2GKeyboard : nsISupports
 {
   void sendKey(in long keyCode, in long charCode);
 
   // Select the <select> option specified by index.
   // If this method is called on a <select> that support multiple
   // selection, then the option specified by index will be added to
@@ -98,14 +213,24 @@ interface nsIInputMethodManager : nsISup
   // Ask the OS to hide the current active Keyboard app. (was: |removeFocus()|)
   // OS should ignore this request if the app is currently not the active one.
   // The OS will void the current input context (if it exists).
   // This method belong to |mgmt| because we would like to allow Keyboard to access to
   // this method w/o a input context.
   void hide();
 };
 
-[scriptable, uuid(5c7f4ce1-a946-4adc-89e6-c908226341a0)]
+[scriptable, uuid(4607330d-e7d2-40a4-9eb8-43967eae0142)]
 interface nsIInputMethod : nsISupports
 {
   // Input Method Manager contain a few global methods expose to apps
   readonly attribute nsIInputMethodManager mgmt;
+
+  // Fired when the input context changes, include changes from and to null.
+  // The new InputContext instance will be available in the event object under |inputcontext| property.
+  // When it changes to null it means the app (the user of this API) no longer has the control of the original focused input field.
+  // Note that if the app saves the original context, it might get void; implementation decides when to void the input context.
+  readonly attribute nsIB2GInputContext inputcontext;
+
+  // An "input context" is mapped to a text field that the app is allow to mutate.
+  // this attribute should be null when there is no text field currently focused.
+  attribute nsIDOMEventListener oninputcontextchange;
 };
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "7669b3265def0eed0473acd938897704007afaf3", 
+    "revision": "fd03fbd18a09517bc5eb4e2af62314421ae7124a", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/confvars.sh
+++ b/b2g/confvars.sh
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 MOZ_APP_BASENAME=B2G
 MOZ_APP_VENDOR=Mozilla
 
-MOZ_APP_VERSION=25.0a1
+MOZ_APP_VERSION=26.0a1
 MOZ_APP_UA_NAME=Firefox
 
 MOZ_UA_OS_AGNOSTIC=1
 
 MOZ_B2G_VERSION=1.2.0.0-prerelease
 MOZ_B2G_OS_NAME=Boot2Gecko
 
 MOZ_BRANDING_DIRECTORY=b2g/branding/unofficial
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -190,17 +190,16 @@
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
 @BINPATH@/components/dom_offline.xpt
 @BINPATH@/components/dom_payment.xpt
-@BINPATH@/components/dom_push.xpt
 @BINPATH@/components/dom_json.xpt
 @BINPATH@/components/dom_browserelement.xpt
 @BINPATH@/components/dom_messages.xpt
 @BINPATH@/components/dom_power.xpt
 @BINPATH@/components/dom_quota.xpt
 @BINPATH@/components/dom_range.xpt
 @BINPATH@/components/dom_settings.xpt
 @BINPATH@/components/dom_permissionsettings.xpt
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -330,16 +330,19 @@ pref("browser.download.manager.showWhenS
 pref("browser.download.manager.closeWhenDone", false);
 pref("browser.download.manager.focusWhenStarting", false);
 pref("browser.download.manager.flashCount", 2);
 pref("browser.download.manager.addToRecentDocs", true);
 pref("browser.download.manager.quitBehavior", 0);
 pref("browser.download.manager.scanWhenDone", true);
 pref("browser.download.manager.resumeOnWakeDelay", 10000);
 
+// Enables the asynchronous Downloads API in the Downloads Panel.
+pref("browser.download.useJSTransfer", false);
+
 // This allows disabling the Downloads Panel in favor of the old interface.
 pref("browser.download.useToolkitUI", false);
 
 // This allows disabling the animated notifications shown by
 // the Downloads Indicator when a download starts or completes.
 pref("browser.download.animateNotifications", true);
 
 // This records whether or not the panel has been shown at least once.
@@ -616,16 +619,19 @@ pref("network.protocol-handler.warn-exte
 // It will also try to open link clicks inside the browser before
 // failing over to the system handlers.
 pref("network.protocol-handler.expose-all", true);
 pref("network.protocol-handler.expose.mailto", false);
 pref("network.protocol-handler.expose.news", false);
 pref("network.protocol-handler.expose.snews", false);
 pref("network.protocol-handler.expose.nntp", false);
 
+// Warning for about:networking page
+pref("network.warnOnAboutNetworking", true);
+
 pref("accessibility.typeaheadfind", false);
 pref("accessibility.typeaheadfind.timeout", 5000);
 pref("accessibility.typeaheadfind.linksonly", false);
 pref("accessibility.typeaheadfind.flashBar", 1);
 
 // plugin finder service url
 pref("pfs.datasource.url", "https://pfs.mozilla.org/plugins/PluginFinderService.php?mimetype=%PLUGIN_MIMETYPE%&appID=%APP_ID%&appVersion=%APP_VERSION%&clientOS=%CLIENT_OS%&chromeLocale=%CHROME_LOCALE%&appRelease=%APP_RELEASE%");
 
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -91,17 +91,17 @@
     <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/>
 
     <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
     <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
     <command id="Tools:DevToolbox" oncommand="gDevToolsBrowser.toggleToolboxCommand(gBrowser);"/>
     <command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/>
     <command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/>
     <command id="Tools:ChromeDebugger" oncommand="BrowserDebuggerProcess.init();" disabled="true" hidden="true"/>
-    <command id="Tools:BrowserConsole" oncommand="HUDConsoleUI.toggleBrowserConsole();"/>
+    <command id="Tools:BrowserConsole" oncommand="HUDService.toggleBrowserConsole();"/>
     <command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();" disabled="true" hidden="true"/>
     <command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();" disabled="true" hidden="true"/>
     <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
     <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()" disabled="true" hidden="true"/>
     <command id="Tools:DevToolsConnect" oncommand="gDevToolsBrowser.openConnectScreen(gBrowser)" disabled="true" hidden="true"/>
     <command id="Tools:Sanitize"
      oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/>
     <command id="Tools:PrivateBrowsing"
@@ -171,17 +171,18 @@
     <broadcaster id="workOfflineMenuitemState"/>
     <broadcaster id="socialSidebarBroadcaster" hidden="true"/>
     <broadcaster id="socialActiveBroadcaster" hidden="true"/>
 
     <!-- DevTools broadcasters -->
     <broadcaster id="devtoolsMenuBroadcaster_DevToolbox"
                  label="&devToolboxMenuItem.label;"
                  type="checkbox" autocheck="false"
-                 command="Tools:DevToolbox"/>
+                 command="Tools:DevToolbox"
+                 key="key_devToolboxMenuItem"/>
     <broadcaster id="devtoolsMenuBroadcaster_DevToolbar"
                  label="&devToolbarMenu.label;"
                  type="checkbox" autocheck="false"
                  command="Tools:DevToolbar"
                  key="key_devToolbar"/>
     <broadcaster id="devtoolsMenuBroadcaster_ChromeDebugger"
                  label="&chromeDebuggerMenu.label;"
                  command="Tools:ChromeDebugger"/>
@@ -256,26 +257,37 @@
 #endif
 #ifdef XP_GNOME
     <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/>
     <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/>
 #else
     <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/>
 #endif
     <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/>
+    <key id="key_devToolboxMenuItemF12" keycode="&devToolsCmd.keycode;" keytext="&devToolsCmd.keytext;" command="Tools:DevToolbox"/>
     <key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" command="Tools:BrowserConsole" modifiers="accel,shift"/>
     <key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift"
          keytext="&devToolbar.keytext;" command="Tools:DevToolbarFocus"/>
     <key id="key_responsiveUI" key="&responsiveDesignTool.commandkey;" command="Tools:ResponsiveUI"
 #ifdef XP_MACOSX
         modifiers="accel,alt"
 #else
         modifiers="accel,shift"
 #endif
     />
+
+    <key id="key_devToolboxMenuItem" keycode="&devToolboxMenuItem.keycode;"
+         keytext="&devToolboxMenuItem.keytext;" command="Tools:DevToolbox" key="&devToolboxMenuItem.keytext;"
+#ifdef XP_MACOSX
+        modifiers="accel,alt"
+#else
+        modifiers="accel,shift"
+#endif
+    />
+
     <key id="key_scratchpad" keycode="&scratchpad.keycode;" modifiers="shift"
          keytext="&scratchpad.keytext;" command="Tools:Scratchpad"/>
     <key id="openFileKb" key="&openFileCmd.commandkey;" command="Browser:OpenFile"  modifiers="accel"/>
     <key id="key_savePage" key="&savePageCmd.commandkey;" command="Browser:SavePage" modifiers="accel"/>
     <key id="printKb" key="&printCmd.commandkey;" command="cmd_print"  modifiers="accel"/>
     <key id="key_close" key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
     <key id="key_closeWindow" key="&closeCmd.key;" command="cmd_closeWindow" modifiers="accel,shift"/>
     <key id="key_undo"
--- a/browser/base/content/browser-thumbnails.js
+++ b/browser/base/content/browser-thumbnails.js
@@ -30,21 +30,16 @@ let gBrowserThumbnails = {
    */
   _tabEvents: ["TabClose", "TabSelect"],
 
   init: function Thumbnails_init() {
     // Bug 863512 - Make page thumbnails work in electrolysis
     if (gMultiProcessBrowser)
       return;
 
-    try {
-      if (Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"))
-        return;
-    } catch (e) {}
-
     PageThumbs.addExpirationFilter(this);
     gBrowser.addTabsProgressListener(this);
     Services.prefs.addObserver(this.PREF_DISK_CACHE_SSL, this, false);
 
     this._sslDiskCacheEnabled =
       Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL);
 
     this._tabEvents.forEach(function (aEvent) {
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -4,17 +4,17 @@
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 
 searchbar {
   -moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
 }
 
-browser[remote="true"] {
+.browserStack > browser[remote="true"] {
   -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
 }
 
 tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
 }
 
 .tabbrowser-tabs {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -79,19 +79,16 @@ this.__defineGetter__("PluralForm", func
 this.__defineSetter__("PluralForm", function (val) {
   delete this.PluralForm;
   return this.PluralForm = val;
 });
 
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
   "resource://gre/modules/TelemetryStopwatch.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
-  "resource:///modules/AboutHomeUtils.jsm");
-
 #ifdef MOZ_SERVICES_SYNC
 XPCOMUtils.defineLazyModuleGetter(this, "Weave",
   "resource://services-sync/main.js");
 #endif
 
 XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () {
   let tmp = {};
   Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp);
@@ -754,17 +751,16 @@ var gBrowserInit = {
 
     window.addEventListener("AppCommand", HandleAppCommandEvent, true);
 
     messageManager.loadFrameScript("chrome://browser/content/content.js", true);
     messageManager.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
 
     // initialize observers and listeners
     // and give C++ access to gBrowser
-    gBrowser.init();
     XULBrowserWindow.init();
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(nsIWebNavigation)
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = window.XULBrowserWindow;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
@@ -1101,17 +1097,25 @@ var gBrowserInit = {
     gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false);
 
     // Initialize the download manager some time after the app starts so that
     // auto-resume downloads begin (such as after crashing or quitting with
     // active downloads) and speeds up the first-load of the download manager UI.
     // If the user manually opens the download manager before the timeout, the
     // downloads will start right away, and getting the service again won't hurt.
     setTimeout(function() {
-      Services.downloads;
+      let DownloadsCommon =
+        Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon;
+      if (DownloadsCommon.useJSTransfer) {
+        // Open the data link without initalizing nsIDownloadManager.
+        DownloadsCommon.initializeAllDataLinks();
+      } else {
+        // Initalizing nsIDownloadManager will trigger the data link.
+        Services.downloads;
+      }
       let DownloadTaskbarProgress =
         Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
       DownloadTaskbarProgress.onBrowserWindowLoad(window);
     }, 10000);
 
     // The object handling the downloads indicator is also initialized here in the
     // delayed startup function, but the actual indicator element is not loaded
     // unless there are downloads to be displayed.
@@ -1265,16 +1269,22 @@ var gBrowserInit = {
 #ifdef MOZ_METRO
     gMetroPrefs.prefDomain.forEach(function(prefName) {
       gMetroPrefs.pushDesktopControlledPrefToMetro(prefName);
       Services.prefs.addObserver(prefName, gMetroPrefs, false);
     }, this);
 #endif
 #endif
 
+    if (gMultiProcessBrowser) {
+      // Bug 862519 - Backspace doesn't work in electrolysis builds.
+      // We bypass the problem by disabling the backspace-to-go-back command.
+      document.getElementById("cmd_handleBackspace").setAttribute("disabled", true);
+    }
+
     SessionStore.promiseInitialized.then(() => {
       // Enable the Restore Last Session command if needed
       if (SessionStore.canRestoreLastSession &&
           !PrivateBrowsingUtils.isWindowPrivate(window))
         goSetCommandEnabled("Browser:RestoreLastSession", true);
 
       TabView.init();
 
@@ -2294,74 +2304,16 @@ function SetPageProxyState(aState)
 
 function PageProxyClickHandler(aEvent)
 {
   if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
     middleMousePaste(aEvent);
 }
 
 /**
- *  Handle load of some pages (about:*) so that we can make modifications
- *  to the DOM for unprivileged pages.
- */
-function BrowserOnAboutPageLoad(doc) {
-  if (doc.documentURI.toLowerCase() == "about:home") {
-    // XXX bug 738646 - when Marketplace is launched, remove this statement and
-    // the hidden attribute set on the apps button in aboutHome.xhtml
-    if (getBoolPref("browser.aboutHome.apps", false))
-      doc.getElementById("apps").removeAttribute("hidden");
-
-    let ss = Components.classes["@mozilla.org/browser/sessionstore;1"].
-             getService(Components.interfaces.nsISessionStore);
-    if (ss.canRestoreLastSession &&
-        !PrivateBrowsingUtils.isWindowPrivate(window))
-      doc.getElementById("launcher").setAttribute("session", "true");
-
-    // Inject search engine and snippets URL.
-    let docElt = doc.documentElement;
-    // set the following attributes BEFORE searchEngineURL, which triggers to
-    // show the snippets when it's set.
-    docElt.setAttribute("snippetsURL", AboutHomeUtils.snippetsURL);
-    if (AboutHomeUtils.showKnowYourRights) {
-      docElt.setAttribute("showKnowYourRights", "true");
-      // Set pref to indicate we've shown the notification.
-      let currentVersion = Services.prefs.getIntPref("browser.rights.version");
-      Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true);
-    }
-    docElt.setAttribute("snippetsVersion", AboutHomeUtils.snippetsVersion);
-
-    let updateSearchEngine = function() {
-      let engine = AboutHomeUtils.defaultSearchEngine;
-      docElt.setAttribute("searchEngineName", engine.name);
-      docElt.setAttribute("searchEnginePostData", engine.postDataString || "");
-      // Again, keep the searchEngineURL as the last attribute, because the
-      // mutation observer in aboutHome.js is counting on that.
-      docElt.setAttribute("searchEngineURL", engine.searchURL);
-    };
-    updateSearchEngine();
-
-    // Listen for the event that's triggered when the user changes search engine.
-    // At this point we simply reload about:home to reflect the change.
-    Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false);
-
-    // Remove the observer when the page is reloaded or closed.
-    doc.defaultView.addEventListener("pagehide", function removeObserver() {
-      doc.defaultView.removeEventListener("pagehide", removeObserver);
-      Services.obs.removeObserver(updateSearchEngine, "browser-search-engine-modified");
-    }, false);
-
-#ifdef MOZ_SERVICES_HEALTHREPORT
-    doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) {
-      BrowserSearch.recordSearchInHealthReport(e.detail, "abouthome");
-    }, true, true);
-#endif
-  }
-}
-
-/**
  * Handle command events bubbling up from error page content
  */
 let BrowserOnClick = {
   handleEvent: function BrowserOnClick_handleEvent(aEvent) {
     if (!aEvent.isTrusted || // Don't trust synthetic events
         aEvent.button == 2 || aEvent.target.localName != "button") {
       return;
     }
@@ -2375,19 +2327,16 @@ let BrowserOnClick = {
       this.onAboutCertError(originalTarget, ownerDoc);
     }
     else if (ownerDoc.documentURI.startsWith("about:blocked")) {
       this.onAboutBlocked(originalTarget, ownerDoc);
     }
     else if (ownerDoc.documentURI.startsWith("about:neterror")) {
       this.onAboutNetError(originalTarget, ownerDoc);
     }
-    else if (ownerDoc.documentURI.toLowerCase() == "about:home") {
-      this.onAboutHome(originalTarget, ownerDoc);
-    }
   },
 
   onAboutCertError: function BrowserOnClick_onAboutCertError(aTargetElm, aOwnerDoc) {
     let elmId = aTargetElm.getAttribute("id");
     let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
     let isTopFrame = (aOwnerDoc.defaultView.parent === aOwnerDoc.defaultView);
 
     switch (elmId) {
@@ -2554,59 +2503,16 @@ let BrowserOnClick = {
   },
 
   onAboutNetError: function BrowserOnClick_onAboutNetError(aTargetElm, aOwnerDoc) {
     let elmId = aTargetElm.getAttribute("id");
     if (elmId != "errorTryAgain" || !/e=netOffline/.test(aOwnerDoc.documentURI))
       return;
     Services.io.offline = false;
   },
-
-  onAboutHome: function BrowserOnClick_onAboutHome(aTargetElm, aOwnerDoc) {
-    let elmId = aTargetElm.getAttribute("id");
-
-    switch (elmId) {
-      case "restorePreviousSession":
-        let ss = Cc["@mozilla.org/browser/sessionstore;1"].
-                 getService(Ci.nsISessionStore);
-        if (ss.canRestoreLastSession) {
-          ss.restoreLastSession();
-        }
-        aOwnerDoc.getElementById("launcher").removeAttribute("session");
-        break;
-
-      case "downloads":
-        BrowserDownloadsUI();
-        break;
-
-      case "bookmarks":
-        PlacesCommandHook.showPlacesOrganizer("AllBookmarks");
-        break;
-
-      case "history":
-        PlacesCommandHook.showPlacesOrganizer("History");
-        break;
-
-      case "apps":
-        openUILinkIn("https://marketplace.mozilla.org/", "tab");
-        break;
-
-      case "addons":
-        BrowserOpenAddonsMgr();
-        break;
-
-      case "sync":
-        openPreferences("paneSync");
-        break;
-
-      case "settings":
-        openPreferences();
-        break;
-    }
-  },
 };
 
 /**
  * Re-direct the browser to a known-safe page.  This function is
  * used when, for example, the user browses to a known malware page
  * and is presented with about:blocked.  The "Get me out of here!"
  * button should take the user to the default start page so that even
  * when their own homepage is infected, we can get them somewhere safe.
@@ -4276,32 +4182,30 @@ var TabsProgressListener = {
     // document URI is not yet the about:-uri of the error page.
 
     let doc = gMultiProcessBrowser ? null : aWebProgress.DOMWindow.document;
     if (!gMultiProcessBrowser &&
         aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
         Components.isSuccessCode(aStatus) &&
         doc.documentURI.startsWith("about:") &&
         !doc.documentURI.toLowerCase().startsWith("about:blank") &&
+        !doc.documentURI.toLowerCase().startsWith("about:home") &&
         !doc.documentElement.hasAttribute("hasBrowserHandlers")) {
       // STATE_STOP may be received twice for documents, thus store an
       // attribute to ensure handling it just once.
       doc.documentElement.setAttribute("hasBrowserHandlers", "true");
       aBrowser.addEventListener("click", BrowserOnClick, true);
       aBrowser.addEventListener("pagehide", function onPageHide(event) {
         if (event.target.defaultView.frameElement)
           return;
         aBrowser.removeEventListener("click", BrowserOnClick, true);
         aBrowser.removeEventListener("pagehide", onPageHide, true);
         if (event.target.documentElement)
           event.target.documentElement.removeAttribute("hasBrowserHandlers");
       }, true);
-
-      // We also want to make changes to page UI for unprivileged about pages.
-      BrowserOnAboutPageLoad(doc);
     }
   },
 
   onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI,
                               aFlags) {
     // Filter out location changes caused by anchor navigation
     // or history.push/pop/replaceState.
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
@@ -7148,18 +7052,23 @@ var TabContextMenu = {
 };
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevToolsBrowser",
                                   "resource:///modules/devtools/gDevTools.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "HUDConsoleUI", function () {
-  return Cu.import("resource:///modules/HUDService.jsm", {}).HUDService.consoleUI;
+Object.defineProperty(this, "HUDService", {
+  get: function HUDService_getter() {
+    let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+    return devtools.require("devtools/webconsole/hudservice");
+  },
+  configurable: true,
+  enumerable: true
 });
 
 // Prompt user to restart the browser in safe mode
 function safeModeRestart()
 {
   // prompt the user to confirm
   let promptTitle = gNavigatorBundle.getString("safeModeRestartPromptTitle");
   let promptMessage =
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -1047,26 +1047,34 @@
     </vbox>
     <vbox id="browser-border-end" hidden="true" layer="true"/>
   </hbox>
 
   <hbox id="full-screen-warning-container" hidden="true" fadeout="true">
     <hbox style="width: 100%;" pack="center"> <!-- Inner hbox needed due to bug 579776. -->
       <vbox id="full-screen-warning-message" align="center">
         <description id="full-screen-domain-text"/>
-        <description class="full-screen-description" value="&fullscreenExitHint.value;"/>
+        <description class="full-screen-description" value="&fullscreenExitHint2.value;"/>
         <vbox id="full-screen-approval-pane" align="center">
-          <description class="full-screen-description" value="&fullscreenApproval.value;"/>
           <hbox>
+#ifdef XP_UNIX
+            <button label="&fullscreenExitButton.label;"
+                    oncommand="FullScreen.setFullscreenAllowed(false);"
+                    class="full-screen-approval-button"/>
+            <button label="&fullscreenAllowButton.label;"
+                    oncommand="FullScreen.setFullscreenAllowed(true);"
+                    class="full-screen-approval-button"/>
+#else
             <button label="&fullscreenAllowButton.label;"
                     oncommand="FullScreen.setFullscreenAllowed(true);"
                     class="full-screen-approval-button"/>
             <button label="&fullscreenExitButton.label;"
                     oncommand="FullScreen.setFullscreenAllowed(false);"
                     class="full-screen-approval-button"/>
+#endif
           </hbox>
           <checkbox id="full-screen-remember-decision"/>
         </vbox>
       </vbox>
     </hbox>
   </hbox>
 
   <vbox id="browser-bottombox" layer="true">
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -6,16 +6,20 @@
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this,
   "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this,
+  "InsecurePasswordUtils", "resource://gre/modules/InsecurePasswordUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 // Bug 671101 - directly using webNavigation in this context
 // causes docshells to leak
 this.__defineGetter__("webNavigation", function () {
   return docShell.QueryInterface(Ci.nsIWebNavigation);
 });
 
 addMessageListener("WebNavigation:LoadURI", function (message) {
@@ -29,19 +33,165 @@ addMessageListener("Browser:HideSessionR
   let doc = content.document;
   let container;
   if (doc.documentURI.toLowerCase() == "about:home" &&
       (container = doc.getElementById("sessionRestoreContainer"))){
     container.hidden = true;
   }
 });
 
-if (!Services.prefs.getBoolPref("browser.tabs.remote")) {
+if (Services.prefs.getBoolPref("browser.tabs.remote")) {
+  addEventListener("contextmenu", function (event) {
+    sendAsyncMessage("contextmenu", {}, { event: event });
+  }, false);
+} else {
   addEventListener("DOMContentLoaded", function(event) {
     LoginManagerContent.onContentLoaded(event);
   });
+  addEventListener("DOMFormHasPassword", function(event) {
+    InsecurePasswordUtils.checkForInsecurePasswords(event.target);
+  });
   addEventListener("DOMAutoComplete", function(event) {
     LoginManagerContent.onUsernameInput(event);
   });
   addEventListener("blur", function(event) {
     LoginManagerContent.onUsernameInput(event);
   });
 }
+
+let AboutHomeListener = {
+  init: function() {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+
+    addMessageListener("AboutHome:Update", this);
+  },
+
+  receiveMessage: function(aMessage) {
+    switch (aMessage.name) {
+    case "AboutHome:Update":
+      this.onUpdate(aMessage.data);
+      break;
+    }
+  },
+
+  onUpdate: function(aData) {
+    let doc = content.document;
+    if (doc.documentURI.toLowerCase() != "about:home")
+      return;
+
+    if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isWindowPrivate(content))
+      doc.getElementById("launcher").setAttribute("session", "true");
+
+    // Inject search engine and snippets URL.
+    let docElt = doc.documentElement;
+    // set the following attributes BEFORE searchEngineURL, which triggers to
+    // show the snippets when it's set.
+    docElt.setAttribute("snippetsURL", aData.snippetsURL);
+    if (aData.showKnowYourRights)
+      docElt.setAttribute("showKnowYourRights", "true");
+    docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
+
+    let engine = aData.defaultSearchEngine;
+    docElt.setAttribute("searchEngineName", engine.name);
+    docElt.setAttribute("searchEnginePostData", engine.postDataString || "");
+    // Again, keep the searchEngineURL as the last attribute, because the
+    // mutation observer in aboutHome.js is counting on that.
+    docElt.setAttribute("searchEngineURL", engine.searchURL);
+  },
+
+  onPageLoad: function(aDocument) {
+    // XXX bug 738646 - when Marketplace is launched, remove this statement and
+    // the hidden attribute set on the apps button in aboutHome.xhtml
+    if (Services.prefs.getPrefType("browser.aboutHome.apps") == Services.prefs.PREF_BOOL &&
+        Services.prefs.getBoolPref("browser.aboutHome.apps"))
+      doc.getElementById("apps").removeAttribute("hidden");
+
+    sendAsyncMessage("AboutHome:RequestUpdate");
+
+    aDocument.addEventListener("AboutHomeSearchEvent", function onSearch(e) {
+      sendAsyncMessage("AboutHome:Search", { engineName: e.detail });
+    }, true, true);
+  },
+
+  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+    let doc = aWebProgress.DOMWindow.document;
+    if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
+        Components.isSuccessCode(aStatus) &&
+        doc.documentURI.toLowerCase() == "about:home" &&
+        !doc.documentElement.hasAttribute("hasBrowserHandlers")) {
+      // STATE_STOP may be received twice for documents, thus store an
+      // attribute to ensure handling it just once.
+      doc.documentElement.setAttribute("hasBrowserHandlers", "true");
+      addEventListener("click", this.onClick, true);
+      addEventListener("pagehide", function onPageHide(event) {
+        if (event.target.defaultView.frameElement)
+          return;
+        removeEventListener("click", this.onClick, true);
+        removeEventListener("pagehide", onPageHide, true);
+        if (event.target.documentElement)
+          event.target.documentElement.removeAttribute("hasBrowserHandlers");
+      }, true);
+
+      // We also want to make changes to page UI for unprivileged about pages.
+      this.onPageLoad(doc);
+    }
+  },
+
+  onClick: function(aEvent) {
+    if (!aEvent.isTrusted || // Don't trust synthetic events
+        aEvent.button == 2 || aEvent.target.localName != "button") {
+      return;
+    }
+
+    let originalTarget = aEvent.originalTarget;
+    let ownerDoc = originalTarget.ownerDocument;
+    let elmId = originalTarget.getAttribute("id");
+
+    switch (elmId) {
+      case "restorePreviousSession":
+        sendAsyncMessage("AboutHome:RestorePreviousSession");
+        ownerDoc.getElementById("launcher").removeAttribute("session");
+        break;
+
+      case "downloads":
+        sendAsyncMessage("AboutHome:Downloads");
+        break;
+
+      case "bookmarks":
+        sendAsyncMessage("AboutHome:Bookmarks");
+        break;
+
+      case "history":
+        sendAsyncMessage("AboutHome:History");
+        break;
+
+      case "apps":
+        sendAsyncMessage("AboutHome:Apps");
+        break;
+
+      case "addons":
+        sendAsyncMessage("AboutHome:Addons");
+        break;
+
+      case "sync":
+        sendAsyncMessage("AboutHome:Sync");
+        break;
+
+      case "settings":
+        sendAsyncMessage("AboutHome:Settings");
+        break;
+    }
+  },
+
+  QueryInterface: function QueryInterface(aIID) {
+    if (aIID.equals(Ci.nsIWebProgressListener) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports)) {
+      return this;
+    }
+
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  }
+};
+AboutHomeListener.init();
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -139,20 +139,16 @@ Site.prototype = {
   },
 
   /**
    * Refreshes the thumbnail for the site.
    */
   refreshThumbnail: function Site_refreshThumbnail() {
     let thumbnailURL = PageThumbs.getThumbnailURL(this.url);
     let thumbnail = this._querySelector(".newtab-thumbnail");
-    // if this is being called due to the thumbnail being updated we will
-    // be setting it to the same value it had before.  To be confident the
-    // change wont be optimized away we remove the property first.
-    thumbnail.style.removeProperty("backgroundImage");
     thumbnail.style.backgroundImage = "url(" + thumbnailURL + ")";
   },
 
   /**
    * Adds event handlers for the site and its buttons.
    */
   _addEventHandlers: function Site_addEventHandlers() {
     // Register drag-and-drop event handlers.
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1,14 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
+var gContextMenuContentData = null;
+
 function nsContextMenu(aXulMenu, aIsShift) {
   this.shouldDisplay = true;
   this.initMenu(aXulMenu, aIsShift);
 }
 
 // Prototype for nsContextMenu "class."
 nsContextMenu.prototype = {
   initMenu: function CM_initMenu(aXulMenu, aIsShift) {
@@ -34,16 +36,17 @@ nsContextMenu.prototype = {
     this.isContentSelected = this.isContentSelection();
     this.onPlainTextLink = false;
 
     // Initialize (disable/remove) menu items.
     this.initItems();
   },
 
   hiding: function CM_hiding() {
+    gContextMenuContentData = null;
     InlineSpellCheckerUI.clearSuggestionsFromMenu();
     InlineSpellCheckerUI.clearDictionaryListFromMenu();
     InlineSpellCheckerUI.uninit();
   },
 
   initItems: function CM_initItems() {
     this.initPageMenuSeparator();
     this.initOpenItems();
@@ -495,16 +498,25 @@ nsContextMenu.prototype = {
     return gDevTools.showToolbox(tt, "inspector").then(function(toolbox) {
       let inspector = toolbox.getCurrentPanel();
       inspector.selection.setNode(this.target, "browser-context-menu");
     }.bind(this));
   },
 
   // Set various context menu attributes based on the state of the world.
   setTarget: function (aNode, aRangeParent, aRangeOffset) {
+    // If gContextMenuContentData is not null, this event was forwarded from a
+    // child process, so use that information instead.
+    if (gContextMenuContentData) {
+      this.isRemote = true;
+      aNode = gContextMenuContentData.event.target;
+    } else {
+      this.isRemote = false;
+    }
+
     const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     if (aNode.namespaceURI == xulNS ||
         aNode.nodeType == Node.DOCUMENT_NODE ||
         this.isDisabledForEvents(aNode)) {
       this.shouldDisplay = false;
       return;
     }
 
@@ -534,21 +546,27 @@ nsContextMenu.prototype = {
     this.bgImageURL        = "";
     this.onEditableArea    = false;
     this.isDesignMode      = false;
     this.onCTPPlugin       = false;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
-    this.browser = this.target.ownerDocument.defaultView
-                                .QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIWebNavigation)
-                                .QueryInterface(Ci.nsIDocShell)
-                                .chromeEventHandler;
+    // If this is a remote context menu event, use the information from
+    // gContextMenuContentData instead.
+    if (this.isRemote) {
+      this.browser = gContextMenuContentData.browser;
+    } else {
+      this.browser = this.target.ownerDocument.defaultView
+                                  .QueryInterface(Ci.nsIInterfaceRequestor)
+                                  .getInterface(Ci.nsIWebNavigation)
+                                  .QueryInterface(Ci.nsIDocShell)
+                                  .chromeEventHandler;
+    }
     this.onSocial = !!this.browser.getAttribute("origin");
 
     // Check if we are in a synthetic document (stand alone image, video, etc.).
     this.inSyntheticDoc =  this.target.ownerDocument.mozSyntheticDocument;
     // First, do checks for nodes that never have children.
     if (this.target.nodeType == Node.ELEMENT_NODE) {
       // See if the user clicked on an image.
       if (this.target instanceof Ci.nsIImageLoadingContent &&
@@ -767,48 +785,60 @@ nsContextMenu.prototype = {
     // until we do.
     return this.linkProtocol && !(
              this.linkProtocol == "mailto"     ||
              this.linkProtocol == "javascript" ||
              this.linkProtocol == "news"       ||
              this.linkProtocol == "snews"      );
   },
 
+  _unremotePrincipal: function(aRemotePrincipal) {
+    if (this.isRemote) {
+      return Cc["@mozilla.org/scriptsecuritymanager;1"]
+               .getService(Ci.nsIScriptSecurityManager)
+               .getAppCodebasePrincipal(aRemotePrincipal.URI,
+                                        aRemotePrincipal.appId,
+                                        aRemotePrincipal.isInBrowserElement);
+    }
+
+    return aRemotePrincipal;
+  },
+
   // Open linked-to URL in a new window.
   openLink : function () {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, doc.nodePrincipal);
+    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
     openLinkIn(this.linkURL, "window",
                { charset: doc.characterSet,
                  referrerURI: doc.documentURIObject });
   },
 
   // Open linked-to URL in a new private window.
   openLinkInPrivateWindow : function () {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, doc.nodePrincipal);
+    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
     openLinkIn(this.linkURL, "window",
                { charset: doc.characterSet,
                  referrerURI: doc.documentURIObject,
                  private: true });
   },
 
   // Open linked-to URL in a new tab.
   openLinkInTab: function() {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, doc.nodePrincipal);
+    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
     openLinkIn(this.linkURL, "tab",
                { charset: doc.characterSet,
                  referrerURI: doc.documentURIObject });
   },
 
   // open URL in current tab
   openLinkInCurrent: function() {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, doc.nodePrincipal);
+    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
     openLinkIn(this.linkURL, "current",
                { charset: doc.characterSet,
                  referrerURI: doc.documentURIObject });
   },
 
   // Open frame in a new tab.
   openFrameInTab: function() {
     var doc = this.target.ownerDocument;
@@ -834,17 +864,18 @@ nsContextMenu.prototype = {
                  referrerURI: referrer ? makeURI(referrer) : null });
   },
 
   // Open clicked-in frame in the same window.
   showOnlyThisFrame: function() {
     var doc = this.target.ownerDocument;
     var frameURL = doc.location.href;
 
-    urlSecurityCheck(frameURL, this.browser.contentPrincipal,
+    urlSecurityCheck(frameURL,
+                     this._unremotePrincipal(this.browser.contentPrincipal),
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     var referrer = doc.referrer;
     openUILinkIn(frameURL, "current", { disallowInheritPrincipal: true,
                                         referrerURI: referrer ? makeURI(referrer) : null });
   },
 
   reload: function(event) {
     if (this.onSocial) {
@@ -897,55 +928,57 @@ nsContextMenu.prototype = {
 
   viewImageInfo: function() {
     BrowserPageInfo(this.target.ownerDocument.defaultView.top.document,
                     "mediaTab", this.target);
   },
 
   viewImageDesc: function(e) {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.imageDescURL, this.browser.contentPrincipal,
+    urlSecurityCheck(this.imageDescURL,
+                     this._unremotePrincipal(this.browser.contentPrincipal),
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
                              referrerURI: doc.documentURIObject });
   },
 
   viewFrameInfo: function() {
     BrowserPageInfo(this.target.ownerDocument);
   },
 
   reloadImage: function(e) {
     urlSecurityCheck(this.mediaURL,
-                     this.browser.contentPrincipal,
+                     this._unremotePrincipal(this.browser.contentPrincipal),
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
 
     if (this.target instanceof Ci.nsIImageLoadingContent)
       this.target.forceReload();
   },
 
   // Change current window to the URL of the image, video, or audio.
   viewMedia: function(e) {
     var viewURL;
 
     if (this.onCanvas)
       viewURL = this.target.toDataURL();
     else {
       viewURL = this.mediaURL;
       urlSecurityCheck(viewURL,
-                       this.browser.contentPrincipal,
+                       this._unremotePrincipal(this.browser.contentPrincipal),
                        Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     }
 
     var doc = this.target.ownerDocument;
     openUILink(viewURL, e, { disallowInheritPrincipal: true,
                              referrerURI: doc.documentURIObject });
   },
 
   saveVideoFrameAsImage: function () {
-    urlSecurityCheck(this.mediaURL, this.browser.contentPrincipal,
+    urlSecurityCheck(this.mediaURL,
+                     this._unremotePrincipal(this.browser.contentPrincipal),
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     let name = "";
     try {
       let uri = makeURI(this.mediaURL);
       let url = uri.QueryInterface(Ci.nsIURL);
       if (url.fileBaseName)
         name = decodeURI(url.fileBaseName) + ".jpg";
     } catch (e) { }
@@ -968,17 +1001,17 @@ nsContextMenu.prototype = {
 
   leaveDOMFullScreen: function() {
     document.mozCancelFullScreen();
   },
 
   // Change current window to the URL of the background image.
   viewBGImage: function(e) {
     urlSecurityCheck(this.bgImageURL,
-                     this.browser.contentPrincipal,
+                     this._unremotePrincipal(this.browser.contentPrincipal),
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     var doc = this.target.ownerDocument;
     openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
                                      referrerURI: doc.documentURIObject });
   },
 
   disableSetDesktopBackground: function() {
     // Disable the Set as Desktop Background menu item if we're still trying
@@ -1002,18 +1035,19 @@ nsContextMenu.prototype = {
   },
 
   setDesktopBackground: function() {
     // Paranoia: check disableSetDesktopBackground again, in case the
     // image changed since the context menu was initiated.
     if (this.disableSetDesktopBackground())
       return;
 
+    var doc = this.target.ownerDocument;
     urlSecurityCheck(this.target.currentURI.spec,
-                     this.target.ownerDocument.nodePrincipal);
+                     this._unremotePrincipal(doc.nodePrincipal));
 
     // Confirm since it's annoying if you hit this accidentally.
     const kDesktopBackgroundURL = 
                   "chrome://browser/content/setDesktopBackground.xul";
 #ifdef XP_MACOSX
     // On Mac, the Set Desktop Background window is not modal.
     // Don't open more than one Set Desktop Background window.
     var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
@@ -1180,17 +1214,17 @@ nsContextMenu.prototype = {
   saveLink: function() {
     var doc =  this.target.ownerDocument;
     var linkText;
     // If selected text is found to match valid URL pattern.
     if (this.onPlainTextLink)
       linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim();
     else
       linkText = this.linkText();
-    urlSecurityCheck(this.linkURL, doc.nodePrincipal);
+    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
 
     this.saveHelper(this.linkURL, linkText, null, true, doc);
   },
 
   // Backwards-compatibility wrapper
   saveImage : function() {
     if (this.onCanvas || this.onImage)
         this.saveMedia();
@@ -1200,22 +1234,24 @@ nsContextMenu.prototype = {
   saveMedia: function() {
     var doc =  this.target.ownerDocument;
     if (this.onCanvas) {
       // Bypass cache, since it's a data: URL.
       saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
                    true, false, doc.documentURIObject, doc);
     }
     else if (this.onImage) {
-      urlSecurityCheck(this.mediaURL, doc.nodePrincipal);
+      urlSecurityCheck(this.mediaURL,
+                       this._unremotePrincipal(doc.nodePrincipal));
       saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, doc.documentURIObject, doc);
     }
     else if (this.onVideo || this.onAudio) {
-      urlSecurityCheck(this.mediaURL, doc.nodePrincipal);
+      urlSecurityCheck(this.mediaURL,
+                       this._unremotePrincipal(doc.nodePrincipal));
       var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
       this.saveHelper(this.mediaURL, null, dialogTitle, false, doc);
     }
   },
 
   // Backwards-compatibility wrapper
   sendImage : function() {
     if (this.onCanvas || this.onImage)
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4,16 +4,21 @@
    - 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/. -->
 
 <!DOCTYPE bindings [
 <!ENTITY % tabBrowserDTD SYSTEM "chrome://browser/locale/tabbrowser.dtd" >
 %tabBrowserDTD;
 ]>
 
+# MAKE_E10S_WORK surrounds code needed to have the front-end try to be smart
+# about using non-remote browsers for loading certain URIs when remote tabs
+# (browser.tabs.remote) are enabled.
+#define MAKE_E10S_WORK 1
+
 <bindings id="tabBrowserBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="tabbrowser">
     <resources>
       <stylesheet src="chrome://browser/content/tabbrowser.css"/>
@@ -164,16 +169,20 @@
           // Force a style flush to ensure that our binding is attached.
           findBar.clientTop;
 
           findBar.browser = browser;
           findBar._findField.value = this._lastFindValue;
 
           aTab._findBar = findBar;
 
+          let event = document.createEvent("Events");
+          event.initEvent("TabFindInitialized", true, false);
+          aTab.dispatchEvent(event);
+
           return findBar;
         ]]></body>
       </method>
 
       <method name="updateWindowResizers">
         <body><![CDATA[
           if (!window.gShowPageResizers)
             return;
@@ -1295,16 +1304,83 @@
               this.selectedTab = firstTabAdded;
             }
             else
               this.selectedBrowser.focus();
           }
         ]]></body>
       </method>
 
+#ifdef MAKE_E10S_WORK
+      <method name="_updateBrowserRemoteness">
+        <parameter name="aBrowser"/>
+        <parameter name="aRemote"/>
+        <body>
+          <![CDATA[
+            let isRemote = aBrowser.getAttribute("remote") == "true";
+            if (isRemote == aRemote)
+              return;
+
+            // Unhook our progress listener.
+            let tab = this._getTabForBrowser(aBrowser);
+            let index = tab._tPos;
+            let filter = this.mTabFilters[index];
+            aBrowser.webProgress.removeProgressListener(filter);
+
+            // Change the "remote" attribute.
+            let parent = aBrowser.parentNode;
+            parent.removeChild(aBrowser);
+            aBrowser.setAttribute("remote", aRemote ? "true" : "false");
+            parent.appendChild(aBrowser);
+
+            // Restore the progress listener.
+            aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+
+            if (aRemote)
+              tab.setAttribute("remote", "true");
+            else
+              tab.removeAttribute("remote");
+          ]]>
+        </body>
+      </method>
+
+      <!--
+        Returns true if we want to load the content for this URL in a
+        remote process. Eventually this should just check whether aURL
+        is unprivileged. Right now, though, we would like to load
+        some unprivileged URLs (like about:neterror) in the main
+        process since they interact with chrome code through
+        BrowserOnClick.
+      -->
+      <method name="_shouldBrowserBeRemote">
+        <parameter name="aURL"/>
+        <body>
+          <![CDATA[
+            if (!gMultiProcessBrowser)
+              return false;
+
+            // loadURI in browser.xml treats null as about:blank
+            if (!aURL)
+              aURL = "about:blank";
+
+            if (aURL.startsWith("about:") &&
+                aURL.toLowerCase() != "about:home" &&
+                aURL.toLowerCase() != "about:blank") {
+              return false;
+            }
+
+            if (aURL.startsWith("chrome:"))
+              return false;
+
+            return true;
+          ]]>
+        </body>
+      </method>
+#endif
+
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <parameter name="aPostData"/>
         <parameter name="aOwner"/>
         <parameter name="aAllowThirdPartyFixup"/>
         <body>
@@ -1341,16 +1417,24 @@
               t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle"));
             else
               t.setAttribute("label", aURI);
 
             t.setAttribute("crop", "end");
             t.setAttribute("onerror", "this.removeAttribute('image');");
             t.className = "tabbrowser-tab";
 
+#ifdef MAKE_E10S_WORK
+            let remote = this._shouldBrowserBeRemote(aURI);
+#else
+            let remote = gMultiProcessBrowser;
+#endif
+            if (remote)
+              t.setAttribute("remote", "true");
+
             this.tabContainer._unlockTabSizing();
 
             // When overflowing, new tabs are scrolled into view smoothly, which
             // doesn't go well together with the width transition. So we skip the
             // transition in that case.
             let animate = !aSkipAnimation &&
                           this.tabContainer.getAttribute("overflow") != "true" &&
                           Services.prefs.getBoolPref("browser.tabs.animate");
@@ -1374,20 +1458,18 @@
             var b = document.createElementNS(
               "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
                                              "browser");
             b.setAttribute("type", "content-targetable");
             b.setAttribute("message", "true");
             b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
             b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
 
-            if (Services.prefs.getPrefType("browser.tabs.remote") == Services.prefs.PREF_BOOL &&
-                Services.prefs.getBoolPref("browser.tabs.remote")) {
+            if (remote)
               b.setAttribute("remote", "true");
-            }
 
             if (window.gShowPageResizers && document.getElementById("addon-bar").collapsed &&
                 window.windowState == window.STATE_NORMAL) {
               b.setAttribute("showresizer", "true");
             }
 
             if (this.hasAttribute("autocompletepopup"))
               b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
@@ -2549,31 +2631,53 @@
 
       <!-- throws exception for unknown schemes -->
       <method name="loadURI">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <body>
           <![CDATA[
+#ifdef MAKE_E10S_WORK
+            this._updateBrowserRemoteness(this.mCurrentBrowser, this._shouldBrowserBeRemote(aURI));
+            try {
+#endif
             return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset);
+#ifdef MAKE_E10S_WORK
+            } catch (e) {
+              let url = this.mCurrentBrowser.currentURI.spec;
+              this._updateBrowserRemoteness(this.mCurrentBrowser, this._shouldBrowserBeRemote(url));
+              throw e;
+            }
+#endif
           ]]>
         </body>
       </method>
 
       <!-- throws exception for unknown schemes -->
       <method name="loadURIWithFlags">
         <parameter name="aURI"/>
         <parameter name="aFlags"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <parameter name="aPostData"/>
         <body>
           <![CDATA[
+#ifdef MAKE_E10S_WORK
+            this._updateBrowserRemoteness(this.mCurrentBrowser, this._shouldBrowserBeRemote(aURI));
+            try {
+#endif
             return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData);
+#ifdef MAKE_E10S_WORK
+            } catch (e) {
+              let url = this.mCurrentBrowser.currentURI.spec;
+              this._updateBrowserRemoteness(this.mCurrentBrowser, this._shouldBrowserBeRemote(url));
+              throw e;
+            }
+#endif
           ]]>
         </body>
       </method>
 
       <method name="goHome">
         <body>
           <![CDATA[
             return this.mCurrentBrowser.goHome();
@@ -2801,29 +2905,34 @@
           switch (aMessage.name) {
             case "DOMTitleChanged":
               let tab = this._getTabForBrowser(browser);
               if (!tab)
                 return;
               let titleChanged = this.setTabTitle(tab);
               if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
                 tab.setAttribute("titlechanged", "true");
+              break;
+            case "contextmenu":
+              gContextMenuContentData = { event: aMessage.objects.event,
+                                          browser: browser };
+              let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
+              popup.openPopup(browser, "overlap",
+                              gContextMenuContentData.event.clientX,
+                              gContextMenuContentData.event.clientY,
+                              true, false, null);
+              break;
           }
         ]]></body>
       </method>
 
       <constructor>
         <![CDATA[
           let browserStack = document.getAnonymousElementByAttribute(this, "anonid", "browserStack");
           this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser");
-          if (Services.prefs.getBoolPref("browser.tabs.remote")) {
-            browserStack.removeChild(this.mCurrentBrowser);
-            this.mCurrentBrowser.setAttribute("remote", true);
-            browserStack.appendChild(this.mCurrentBrowser);
-          }
 
           this.mCurrentTab = this.tabContainer.firstChild;
           document.addEventListener("keypress", this, false);
           window.addEventListener("sizemodechange", this, false);
 
           var uniqueId = "panel" + Date.now();
           this.mPanelContainer.childNodes[0].id = uniqueId;
           this.mCurrentTab.linkedPanel = uniqueId;
@@ -2842,39 +2951,62 @@
           // Hook up the event listeners to the first browser
           var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true);
           const nsIWebProgress = Components.interfaces.nsIWebProgress;
           const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
                                    .createInstance(nsIWebProgress);
           filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
           this.mTabListeners[0] = tabListener;
           this.mTabFilters[0] = filter;
-          this.init();
+
+          try {
+            // We assume this can only fail because mCurrentBrowser's docShell
+            // hasn't been created, yet. This may be caused by code accessing
+            // gBrowser before the window has finished loading.
+            this._addProgressListenerForInitialTab();
+          } catch (e) {
+            // The binding was constructed too early, wait until the initial
+            // tab's document is ready, then add the progress listener.
+            this._waitForInitialContentDocument();
+          }
 
           this.style.backgroundColor =
             Services.prefs.getBoolPref("browser.display.use_system_colors") ?
               "-moz-default-background-color" :
               Services.prefs.getCharPref("browser.display.background_color");
 
-          if (Services.prefs.getBoolPref("browser.tabs.remote"))
+          if (Services.prefs.getBoolPref("browser.tabs.remote")) {
             messageManager.addMessageListener("DOMTitleChanged", this);
+            messageManager.addMessageListener("contextmenu", this);
+          }
         ]]>
       </constructor>
 
-      <method name="init">
+      <method name="_addProgressListenerForInitialTab">
+        <body><![CDATA[
+          this.webProgress.addProgressListener(this.mTabFilters[0], Ci.nsIWebProgress.NOTIFY_ALL);
+        ]]></body>
+      </method>
+
+      <method name="_waitForInitialContentDocument">
         <body><![CDATA[
-          if (!this._initialProgressListenerAdded) {
-            this._initialProgressListenerAdded = true;
-            try {
-              this.webProgress.addProgressListener(this.mTabFilters[0], Components.interfaces.nsIWebProgress.NOTIFY_ALL);
-            } catch (e) {
-              // The binding was constructed too early, need to try this again later. See bug 463384.
-              this._initialProgressListenerAdded = false;
+          let obs = (subject, topic) => {
+            if (this.browsers[0].contentWindow == subject) {
+              Services.obs.removeObserver(obs, topic);
+              this._addProgressListenerForInitialTab();
             }
-          }
+          };
+
+          // We use content-document-global-created as an approximation for
+          // "docShell is initialized". We can do this because in the
+          // mTabProgressListener we care most about the STATE_STOP notification
+          // that will reset mBlank. That means it's important to at least add
+          // the progress listener before the initial about:blank load stops
+          // if we can't do it before the load starts.
+          Services.obs.addObserver(obs, "content-document-global-created", false);
         ]]></body>
       </method>
 
       <destructor>
         <![CDATA[
           for (var i = 0; i < this.mTabListeners.length; ++i) {
             let browser = this.getBrowserAtIndex(i);
             if (browser.registeredOpenURI) {
@@ -2885,18 +3017,20 @@
             this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]);
             this.mTabFilters[i] = null;
             this.mTabListeners[i].destroy();
             this.mTabListeners[i] = null;
           }
           document.removeEventListener("keypress", this, false);
           window.removeEventListener("sizemodechange", this, false);
 
-          if (Services.prefs.getBoolPref("browser.tabs.remote"))
+          if (Services.prefs.getBoolPref("browser.tabs.remote")) {
             messageManager.removeMessageListener("DOMTitleChanged", this);
+            messageManager.removeMessageListener("contextmenu", this);
+          }
         ]]>
       </destructor>
 
       <!-- Deprecated stuff, implemented for backwards compatibility. -->
       <method name="enterTabbedMode">
         <body>
           Application.console.log("enterTabbedMode is an obsolete method and " +
                                   "will be removed in a future release.");
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -179,16 +179,17 @@ MOCHITEST_BROWSER_FILES = \
                  browser_bug812562.js \
                  browser_bug816527.js \
                  browser_bug817947.js \
                  browser_bug818118.js \
                  browser_bug820497.js \
                  browser_bug822367.js \
                  browser_bug832435.js \
                  browser_bug839103.js \
+                 browser_bug880101.js \
                  browser_bug882977.js \
                  browser_bug887515.js \
                  browser_canonizeURL.js \
                  browser_clearplugindata_noage.html \
                  browser_clearplugindata.html \
                  browser_clearplugindata.js \
                  browser_contentAreaClick.js \
                  browser_contextSearchTabPosition.js \
--- a/browser/base/content/test/browser_CTP_data_urls.js
+++ b/browser/base/content/test/browser_CTP_data_urls.js
@@ -90,26 +90,42 @@ function pageLoad() {
   executeSoon(gNextTest);
 }
 
 function prepareTest(nextTest, url) {
   gNextTest = nextTest;
   gTestBrowser.contentWindow.location = url;
 }
 
+// Due to layout being async, "PluginBindAttached" may trigger later.
+// This wraps a function to force a layout flush, thus triggering it,
+// and schedules the function execution so they're definitely executed
+// afterwards.
+function runAfterPluginBindingAttached(func) {
+  let doc = gTestBrowser.contentDocument;
+  return function() {
+    let elems = doc.getElementsByTagName('embed');
+    if (elems.length < 1) {
+      elems = doc.getElementsByTagName('object');
+    }
+    elems[0].clientTop;
+    executeSoon(func);
+  };
+}
+
 // Test that the click-to-play doorhanger still works when navigating to data URLs
 function test1a() {
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 1a, Should have a click-to-play notification");
 
   let plugin = gTestBrowser.contentDocument.getElementById("test");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(!objLoadingContent.activated, "Test 1a, Plugin should not be activated");
 
-  gNextTest = test1b;
+  gNextTest = runAfterPluginBindingAttached(test1b);
   gTestBrowser.contentDocument.getElementById("data-link-1").click();
 }
 
 function test1b() {
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 1b, Should have a click-to-play notification");
 
   let plugin = gTestBrowser.contentDocument.getElementById("test");
@@ -121,28 +137,28 @@ function test1b() {
   PopupNotifications.panel.firstChild._primaryButton.click();
 
   let condition = function() objLoadingContent.activated;
   waitForCondition(condition, test1c, "Test 1b, Waited too long for plugin to activate");
 }
 
 function test1c() {
   clearAllPluginPermissions();
-  prepareTest(test2a, gHttpTestRoot + "plugin_data_url.html");
+  prepareTest(runAfterPluginBindingAttached(test2a), gHttpTestRoot + "plugin_data_url.html");
 }
 
 // Test that the click-to-play notification doesn't break when navigating to data URLs with multiple plugins
 function test2a() {
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 2a, Should have a click-to-play notification");
   let plugin = gTestBrowser.contentDocument.getElementById("test");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(!objLoadingContent.activated, "Test 2a, Plugin should not be activated");
 
-  gNextTest = test2b;
+  gNextTest = runAfterPluginBindingAttached(test2b);
   gTestBrowser.contentDocument.getElementById("data-link-2").click();
 }
 
 function test2b() {
   let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(notification, "Test 2b, Should have a click-to-play notification");
 
   // Simulate choosing "Allow now" for the test plugin
@@ -179,17 +195,17 @@ function test2b() {
 }
 
 function test2c() {
   let plugin = gTestBrowser.contentDocument.getElementById("test1");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(objLoadingContent.activated, "Test 2c, Plugin should be activated");
 
   clearAllPluginPermissions();
-  prepareTest(test3a, gHttpTestRoot + "plugin_data_url.html");
+  prepareTest(runAfterPluginBindingAttached(test3a), gHttpTestRoot + "plugin_data_url.html");
 }
 
 // Test that when navigating to a data url, the plugin permission is inherited
 function test3a() {
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 3a, Should have a click-to-play notification");
   let plugin = gTestBrowser.contentDocument.getElementById("test");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
@@ -209,17 +225,18 @@ function test3b() {
 }
 
 function test3c() {
   let plugin = gTestBrowser.contentDocument.getElementById("test");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(objLoadingContent.activated, "Test 3c, Plugin should be activated");
 
   clearAllPluginPermissions();
-  prepareTest(test4b, 'data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>');
+  prepareTest(runAfterPluginBindingAttached(test4b),
+              'data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>');
 }
 
 // Test that the click-to-play doorhanger still works when directly navigating to data URLs
 function test4a() {
   let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 4a, Should have a click-to-play notification");
   let plugin = gTestBrowser.contentDocument.getElementById("test");
   let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
--- a/browser/base/content/test/browser_aboutHome.js
+++ b/browser/base/content/test/browser_aboutHome.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
-  "resource:///modules/AboutHomeUtils.jsm");
+  "resource:///modules/AboutHome.jsm");
 
 let gRightsVersion = Services.prefs.getIntPref("browser.rights.version");
 
 registerCleanupFunction(function() {
   // Ensure we don't pollute prefs for next tests.
   Services.prefs.clearUserPref("network.cookies.cookieBehavior");
   Services.prefs.clearUserPref("network.cookie.lifetimePolicy");
   Services.prefs.clearUserPref("browser.rights.override");
@@ -101,24 +101,27 @@ let gTests = [
       return Promise.resolve();
     }
 
     let numSearchesBefore = 0;
     let deferred = Promise.defer();
     let doc = gBrowser.contentDocument;
     let engineName = doc.documentElement.getAttribute("searchEngineName");
 
-    // We rely on the listener in browser.js being installed and fired before
-    // this one. If this ever changes, we should add an executeSoon() or similar.
     doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) {
       is(e.detail, engineName, "Detail is search engine name");
 
-      getNumberOfSearches(engineName).then(num => {
-        is(num, numSearchesBefore + 1, "One more search recorded.");
-        deferred.resolve();
+      // We use executeSoon() to ensure that this code runs after the
+      // count has been updated in browser.js, since it uses the same
+      // event.
+      executeSoon(function () {
+        getNumberOfSearches(engineName).then(num => {
+          is(num, numSearchesBefore + 1, "One more search recorded.");
+          deferred.resolve();
+        });
       });
     }, true, true);
 
     // Get the current number of recorded searches.
     getNumberOfSearches(engineName).then(num => {
       numSearchesBefore = num;
 
       info("Perform a search.");
@@ -270,40 +273,52 @@ let gTests = [
       info("Observer: " + aData + " for " + engine.name);
 
       if (aData != "engine-added")
         return;
 
       if (engine.name != "POST Search")
         return;
 
+      // Ready to execute the tests!
+      let needle = "Search for something awesome.";
+      let document = gBrowser.selectedTab.linkedBrowser.contentDocument;
+      let searchText = document.getElementById("searchText");
+
+      // We're about to change the search engine. Once the change has
+      // propagated to the about:home content, we want to perform a search.
+      let mutationObserver = new MutationObserver(function (mutations) {
+        for (let mutation of mutations) {
+          if (mutation.attributeName == "searchEngineURL") {
+            searchText.value = needle;
+            searchText.focus();
+            EventUtils.synthesizeKey("VK_RETURN", {});
+          }
+        }
+      });
+      mutationObserver.observe(document.documentElement, { attributes: true });
+
+      // Change the search engine, triggering the observer above.
       Services.search.defaultEngine = engine;
 
       registerCleanupFunction(function() {
+        mutationObserver.disconnect();
         Services.search.removeEngine(engine);
         Services.search.defaultEngine = currEngine;
       });
 
 
-      // Ready to execute the tests!
-      let needle = "Search for something awesome.";
-      let document = gBrowser.selectedTab.linkedBrowser.contentDocument;
-      let searchText = document.getElementById("searchText");
-
+      // When the search results load, check them for correctness.
       waitForLoad(function() {
         let loadedText = gBrowser.contentDocument.body.textContent;
         ok(loadedText, "search page loaded");
         is(loadedText, "searchterms=" + escape(needle.replace(/\s/g, "+")),
            "Search text should arrive correctly");
         deferred.resolve();
       });
-
-      searchText.value = needle;
-      searchText.focus();
-      EventUtils.synthesizeKey("VK_RETURN", {});
     };
     Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false);
     registerCleanupFunction(function () {
       Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
     });
     Services.search.addEngine("http://test:80/browser/browser/base/content/test/POSTSearchEngine.xml",
                               Ci.nsISearchEngine.DATA_XML, null, false);
     return deferred.promise;
--- a/browser/base/content/test/browser_bug537013.js
+++ b/browser/base/content/test/browser_bug537013.js
@@ -39,36 +39,44 @@ function test() {
   gBrowser.selectedTab = tabs[0];
 
   setFindString(texts[0]);
   // Turn on highlight for testing bug 891638
   gFindBar.getElement("highlight").checked = true;
 
   // Make sure the second tab is correct, then set it up
   gBrowser.selectedTab = tabs[1];
+  gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests1);
+  // Initialize the findbar
+  gFindBar;
+}
+function continueTests1() {
+  gBrowser.selectedTab.removeEventListener("TabFindInitialized",
+                                           continueTests1);
+  ok(true, "'TabFindInitialized' event properly dispatched!");
   ok(gFindBar.hidden, "Second tab doesn't show find bar!");
   gFindBar.open();
   is(gFindBar._findField.value, texts[0],
      "Second tab kept old find value for new initialization!");
   setFindString(texts[1]);
 
   // Confirm the first tab is still correct, ensure re-hiding works as expected
   gBrowser.selectedTab = tabs[0];
   ok(!gFindBar.hidden, "First tab shows find bar!");
   is(gFindBar._findField.value, texts[0], "First tab persists find value!");
   ok(gFindBar.getElement("highlight").checked,
      "Highlight button state persists!");
 
   // While we're here, let's test bug 253793
   gBrowser.reload();
-  gBrowser.addEventListener("DOMContentLoaded", continueTests, true);
+  gBrowser.addEventListener("DOMContentLoaded", continueTests2, true);
 }
 
-function continueTests() {
-  gBrowser.removeEventListener("DOMContentLoaded", continueTests, true);
+function continueTests2() {
+  gBrowser.removeEventListener("DOMContentLoaded", continueTests2, true);
   ok(!gFindBar.getElement("highlight").checked, "Highlight button reset!");
   gFindBar.close();
   ok(gFindBar.hidden, "First tab doesn't show find bar!");
   gBrowser.selectedTab = tabs[1];
   ok(!gFindBar.hidden, "Second tab shows find bar!");
   // Test for bug 892384
   is(gFindBar._findField.getAttribute("focused"), "true",
      "Open findbar refocused on tab change!");
--- a/browser/base/content/test/browser_bug752516.js
+++ b/browser/base/content/test/browser_bug752516.js
@@ -24,18 +24,20 @@ function test() {
   gTestBrowser = gBrowser.selectedBrowser;
   let gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
   gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug752516.html";
 
   gTestBrowser.addEventListener("load", tabLoad, true);
 }
 
 function tabLoad() {
-  // The plugin events are async dispatched and can come after the load event
-  // This just allows the events to fire before we proceed
+  // Due to layout being async, "PluginBindAttached" may trigger later.
+  // This forces a layout flush, thus triggering it, and schedules the
+  // test so it is definitely executed afterwards.
+  gTestBrowser.contentDocument.getElementById('test').clientTop;
   executeSoon(actualTest);
 }
 
 function actualTest() {
   let doc = gTestBrowser.contentDocument;
   let plugin = doc.getElementById("test");
   ok(!plugin.activated, "Plugin should not be activated");
   ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Doorhanger should not be open");
--- a/browser/base/content/test/browser_bug787619.js
+++ b/browser/base/content/test/browser_bug787619.js
@@ -16,16 +16,20 @@ function test() {
 
   gBrowser.selectedTab = gBrowser.addTab();
   gTestBrowser = gBrowser.selectedBrowser;
   gTestBrowser.addEventListener("load", pageLoad, true);
   gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug787619.html";
 }
 
 function pageLoad() {
+  // Due to layout being async, "PluginBindAttached" may trigger later.
+  // This forces a layout flush, thus triggering it, and schedules the
+  // test so it is definitely executed afterwards.
+  gTestBrowser.contentDocument.getElementById('plugin').clientTop;
   executeSoon(part1);
 }
 
 function part1() {
   let wrapper = gTestBrowser.contentDocument.getElementById('wrapper');
   wrapper.addEventListener('click', function() ++gWrapperClickCount, false);
 
   let plugin = gTestBrowser.contentDocument.getElementById('plugin');
--- a/browser/base/content/test/browser_bug812562.js
+++ b/browser/base/content/test/browser_bug812562.js
@@ -16,17 +16,24 @@ function test() {
   plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
 
   var newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   gTestBrowser = gBrowser.selectedBrowser;
   gTestBrowser.addEventListener("load", pageLoad, true);
   setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml",
   function() {
-    prepareTest(testPart1, gHttpTestRoot + "plugin_test.html");
+    prepareTest(function() {
+        // Due to layout being async, "PluginBindAttached" may trigger later.
+        // This forces a layout flush, thus triggering it, and schedules the
+        // test so it is definitely executed afterwards.
+        gTestBrowser.contentDocument.getElementById('test').clientTop;
+        testPart1();
+      },
+      gHttpTestRoot + "plugin_test.html");
   });
 }
 
 function finishTest() {
   gTestBrowser.removeEventListener("load", pageLoad, true);
   gBrowser.removeCurrentTab();
   window.focus();
   setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml",
--- a/browser/base/content/test/browser_bug818118.js
+++ b/browser/base/content/test/browser_bug818118.js
@@ -18,18 +18,20 @@ function test() {
 
   gBrowser.selectedTab = gBrowser.addTab();
   gTestBrowser = gBrowser.selectedBrowser;
   gTestBrowser.addEventListener("load", pageLoad, true);
   gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_both.html";
 }
 
 function pageLoad(aEvent) {
-  // The plugin events are async dispatched and can come after the load event
-  // This just allows the events to fire before we then go on to test the states
+  // Due to layout being async, "PluginBindAttached" may trigger later.
+  // This forces a layout flush, thus triggering it, and schedules the
+  // test so it is definitely executed afterwards.
+  gTestBrowser.contentDocument.getElementById('test').clientTop;
   executeSoon(actualTest);
 }
 
 function actualTest() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "should have a click-to-play notification");
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   ok(plugin, "should have known plugin in page");
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_bug880101.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "about:robots";
+
+function test() {
+  let win;
+
+  let listener = {
+    onLocationChange: (webProgress, request, uri, flags) => {
+      ok(webProgress.isTopLevel, "Received onLocationChange from top frame");
+      is(uri.spec, URL, "Received onLocationChange for correct URL");
+      finish();
+    }
+  };
+
+  waitForExplicitFinish();
+
+  // Remove the listener and window when we're done.
+  registerCleanupFunction(() => {
+    win.gBrowser.removeProgressListener(listener);
+    win.close();
+  });
+
+  // Wait for the newly opened window.
+  whenNewWindowOpened(w => win = w);
+
+  // Open a link in a new window.
+  openLinkIn(URL, "window", {});
+
+  // On the next tick, but before the window has finished loading, access the
+  // window's gBrowser property to force the tabbrowser constructor early.
+  (function tryAddProgressListener() {
+    executeSoon(() => {
+      try {
+        win.gBrowser.addProgressListener(listener);
+      } catch (e) {
+        // win.gBrowser wasn't ready, yet. Try again in a tick.
+        tryAddProgressListener();
+      }
+    });
+  })();
+}
+
+function whenNewWindowOpened(cb) {
+  Services.obs.addObserver(function obs(win) {
+    Services.obs.removeObserver(obs, "domwindowopened");
+    cb(win);
+  }, "domwindowopened", false);
+}
--- a/browser/base/content/test/browser_pluginnotification.js
+++ b/browser/base/content/test/browser_pluginnotification.js
@@ -89,43 +89,59 @@ function pageLoad() {
   executeSoon(gNextTest);
 }
 
 function prepareTest(nextTest, url) {
   gNextTest = nextTest;
   gTestBrowser.contentWindow.location = url;
 }
 
+// Due to layout being async, "PluginBindAttached" may trigger later.
+// This wraps a function to force a layout flush, thus triggering it,
+// and schedules the function execution so they're definitely executed
+// afterwards.
+function runAfterPluginBindingAttached(func) {
+  let doc = gTestBrowser.contentDocument;
+  return function() {
+    let elems = doc.getElementsByTagName('embed');
+    if (elems.length < 1) {
+      elems = doc.getElementsByTagName('object');
+    }
+    elems[0].clientTop;
+    executeSoon(func);
+  };
+}
+
 // Tests a page with an unknown plugin in it.
 function test1() {
   ok(PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 1, Should have displayed the missing plugin notification");
   ok(gTestBrowser.missingPlugins, "Test 1, Should be a missing plugin list");
   ok(gTestBrowser.missingPlugins.has("application/x-unknown"), "Test 1, Should know about application/x-unknown");
   ok(!gTestBrowser.missingPlugins.has("application/x-test"), "Test 1, Should not know about application/x-test");
 
   var pluginNode = gTestBrowser.contentDocument.getElementById("unknown");
   ok(pluginNode, "Test 1, Found plugin in page");
   var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED, "Test 1, plugin fallback type should be PLUGIN_UNSUPPORTED");
 
   var plugin = getTestPlugin();
   ok(plugin, "Should have a test plugin");
   plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
-  prepareTest(test2, gTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test2), gTestRoot + "plugin_test.html");
 }
 
 // Tests a page with a working plugin in it.
 function test2() {
   ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 2, Should not have displayed the missing plugin notification");
   ok(!gTestBrowser.missingPlugins, "Test 2, Should not be a missing plugin list");
 
   var plugin = getTestPlugin();
   ok(plugin, "Should have a test plugin");
   plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
-  prepareTest(test3, gTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test3), gTestRoot + "plugin_test.html");
 }
 
 // Tests a page with a disabled plugin in it.
 function test3() {
   ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 3, Should not have displayed the missing plugin notification");
   ok(!gTestBrowser.missingPlugins, "Test 3, Should not be a missing plugin list");
 
   new TabOpenListener("about:addons", test4, prepareTest5);
@@ -147,17 +163,17 @@ function test4(tab, win) {
 
 function prepareTest5() {
   info("prepareTest5");
   var plugin = getTestPlugin();
   plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
   setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml",
     function() {
       info("prepareTest5 callback");
-      prepareTest(test5, gTestRoot + "plugin_test.html");
+      prepareTest(runAfterPluginBindingAttached(test5), gTestRoot + "plugin_test.html");
   });
 }
 
 // Tests a page with a blocked plugin in it.
 function test5() {
   info("test5");
   ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 5, Should not have displayed the missing plugin notification");
   let notification = PopupNotifications.getNotification("click-to-play-plugins");
@@ -169,67 +185,67 @@ function test5() {
   ok(PopupNotifications.panel.firstChild._buttonContainer.hidden, "Part 5: The blocked plugins notification should not have any buttons visible.");
 
   ok(!gTestBrowser.missingPlugins, "Test 5, Should not be a missing plugin list");
   var pluginNode = gTestBrowser.contentDocument.getElementById("test");
   ok(pluginNode, "Test 5, Found plugin in page");
   var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED, "Test 5, plugin fallback type should be PLUGIN_BLOCKLISTED");
 
-  prepareTest(test6, gTestRoot + "plugin_both.html");
+  prepareTest(runAfterPluginBindingAttached(test6), gTestRoot + "plugin_both.html");
 }
 
 // Tests a page with a blocked and unknown plugin in it.
 function test6() {
   ok(PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 6, Should have displayed the missing plugin notification");
   ok(gTestBrowser.missingPlugins, "Test 6, Should be a missing plugin list");
   ok(gTestBrowser.missingPlugins.has("application/x-unknown"), "Test 6, Should know about application/x-unknown");
   ok(!gTestBrowser.missingPlugins.has("application/x-test"), "Test 6, application/x-test should not be a missing plugin");
 
-  prepareTest(test7, gTestRoot + "plugin_both2.html");
+  prepareTest(runAfterPluginBindingAttached(test7), gTestRoot + "plugin_both2.html");
 }
 
 // Tests a page with a blocked and unknown plugin in it (alternate order to above).
 function test7() {
   ok(PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 7, Should have displayed the missing plugin notification");
   ok(gTestBrowser.missingPlugins, "Test 7, Should be a missing plugin list");
   ok(gTestBrowser.missingPlugins.has("application/x-unknown"), "Test 7, Should know about application/x-unknown");
   ok(!gTestBrowser.missingPlugins.has("application/x-test"), "Test 7, application/x-test should not be a missing plugin");
 
   var plugin = getTestPlugin();
   plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
   getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
 
   setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", function() {
-    prepareTest(test8, gTestRoot + "plugin_test.html");
+    prepareTest(runAfterPluginBindingAttached(test8), gTestRoot + "plugin_test.html");
   });
 }
 
 // Tests a page with a working plugin that is click-to-play
 function test8() {
   ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 8, Should not have displayed the missing plugin notification");
   ok(!gTestBrowser.missingPlugins, "Test 8, Should not be a missing plugin list");
   ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "Test 8, Should have a click-to-play notification");
 
   var pluginNode = gTestBrowser.contentDocument.getElementById("test");
   ok(pluginNode, "Test 8, Found plugin in page");
   var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 8, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
 
-  prepareTest(test11a, gTestRoot + "plugin_test3.html");
+  prepareTest(runAfterPluginBindingAttached(test11a), gTestRoot + "plugin_test3.html");
 }
 
 // Tests 9 & 10 removed
 
 // Tests that the going back will reshow the notification for click-to-play plugins (part 1/4)
 function test11a() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 11a, Should have a click-to-play notification");
 
-  prepareTest(test11b, "about:blank");
+  prepareTest(runAfterPluginBindingAttached(test11b), "about:blank");
 }
 
 // Tests that the going back will reshow the notification for click-to-play plugins (part 2/4)
 function test11b() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(!popupNotification, "Test 11b, Should not have a click-to-play notification");
 
   Services.obs.addObserver(test11c, "PopupNotifications-updateNotShowing", false);
@@ -243,17 +259,17 @@ function test11c() {
   waitForCondition(condition, test11d, "Test 11c, waited too long for click-to-play-plugin notification");
 }
 
 // Tests that the going back will reshow the notification for click-to-play plugins (part 4/4)
 function test11d() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 11d, Should have a click-to-play notification");
 
-  prepareTest(test12a, gHttpTestRoot + "plugin_clickToPlayAllow.html");
+  prepareTest(runAfterPluginBindingAttached(test12a), gHttpTestRoot + "plugin_clickToPlayAllow.html");
 }
 
 // Tests that the "Allow Always" permission works for click-to-play plugins
 function test12a() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 12a, Should have a click-to-play notification");
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
@@ -265,17 +281,17 @@ function test12a() {
 
   var condition = function() objLoadingContent.activated;
   waitForCondition(condition, test12b, "Test 12a, Waited too long for plugin to activate");
 }
 
 function test12b() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 12d, Should have a click-to-play notification");
-  prepareTest(test12c, gHttpTestRoot + "plugin_two_types.html");
+  prepareTest(runAfterPluginBindingAttached(test12c), gHttpTestRoot + "plugin_two_types.html");
 }
 
 // Test that the "Always" permission, when set for just the Test plugin,
 // does not also allow the Second Test plugin.
 function test12c() {
   var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(popupNotification, "Test 12d, Should have a click-to-play notification");
   var test = gTestBrowser.contentDocument.getElementById("test");
@@ -297,43 +313,43 @@ function test12c() {
 function test14() {
   var plugin = gTestBrowser.contentDocument.getElementById("test1");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(objLoadingContent.activated, "Test 14, Plugin should be activated");
 
   var plugin = getTestPlugin();
   plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
   getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
-  prepareTest(test15, gTestRoot + "plugin_alternate_content.html");
+  prepareTest(runAfterPluginBindingAttached(test15), gTestRoot + "plugin_alternate_content.html");
 }
 
 // Tests that the overlay is shown instead of alternate content when
 // plugins are click to play
 function test15() {
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   var doc = gTestBrowser.contentDocument;
   var mainBox = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
   ok(mainBox, "Test 15, Plugin with id=" + plugin.id + " overlay should exist");
 
-  prepareTest(test17, gTestRoot + "plugin_bug749455.html");
+  prepareTest(runAfterPluginBindingAttached(test17), gTestRoot + "plugin_bug749455.html");
 }
 
 // Test 16 removed
 
 // Tests that mContentType is used for click-to-play plugins, and not the
 // inspected type.
 function test17() {
   var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(clickToPlayNotification, "Test 17, Should have a click-to-play notification");
   var missingNotification = PopupNotifications.getNotification("missing-plugins", gTestBrowser);
   ok(!missingNotification, "Test 17, Should not have a missing plugin notification");
 
   setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml",
   function() {
-    prepareTest(test18a, gHttpTestRoot + "plugin_test.html");
+    prepareTest(runAfterPluginBindingAttached(test18a), gHttpTestRoot + "plugin_test.html");
   });
 }
 
 // Tests a vulnerable, updatable plugin
 function test18a() {
   var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(clickToPlayNotification, "Test 18a, Should have a click-to-play notification");
   var doc = gTestBrowser.contentDocument;
@@ -366,17 +382,17 @@ function test18b() {
   var plugin = doc.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(!objLoadingContent.activated, "Test 18b, Plugin should not be activated");
   var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
   ok(overlay.style.visibility != "hidden", "Test 18b, Plugin overlay should exist, not be hidden");
 
   setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableNoUpdate.xml",
   function() {
-    prepareTest(test18c, gHttpTestRoot + "plugin_test.html");
+    prepareTest(runAfterPluginBindingAttached(test18c), gHttpTestRoot + "plugin_test.html");
   });
 }
 
 // Tests a vulnerable plugin with no update
 function test18c() {
   var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(clickToPlayNotification, "Test 18c, Should have a click-to-play notification");
   var doc = gTestBrowser.contentDocument;
@@ -409,17 +425,17 @@ function test18d() {
 
 // continue testing "Always allow"
 function test18e() {
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(objLoadingContent.activated, "Test 18e, Plugin should be activated");
 
   clearAllPluginPermissions();
-  prepareTest(test18f, gHttpTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test18f), gHttpTestRoot + "plugin_test.html");
 }
 
 // clicking the in-content overlay of a vulnerable plugin should bring
 // up the notification and not directly activate the plugin
 function test18f() {
   var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(notification, "Test 18f, Should have a click-to-play notification");
   ok(notification.dismissed, "Test 18f, notification should start dismissed");
@@ -441,17 +457,17 @@ function test18g() {
   notification.options.eventCallback = null;
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(!objLoadingContent.activated, "Test 18g, Plugin should not be activated");
 
   setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml",
   function() {
     resetBlocklist();
-    prepareTest(test19a, gTestRoot + "plugin_test.html");
+    prepareTest(runAfterPluginBindingAttached(test19a), gTestRoot + "plugin_test.html");
   });
 }
 
 // Tests that clicking the icon of the overlay activates the doorhanger
 function test19a() {
   var doc = gTestBrowser.contentDocument;
   var plugin = doc.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
@@ -460,17 +476,17 @@ function test19a() {
 
   var icon = doc.getAnonymousElementByAttribute(plugin, "class", "icon");
   EventUtils.synthesizeMouseAtCenter(icon, {}, gTestBrowser.contentWindow);
   let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed;
   waitForCondition(condition, test19b, "Test 19a, Waited too long for doorhanger to activate");
 }
 
 function test19b() {
-  prepareTest(test19c, gTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test19c), gTestRoot + "plugin_test.html");
 }
 
 // Tests that clicking the text of the overlay activates the plugin
 function test19c() {
   var doc = gTestBrowser.contentDocument;
   var plugin = doc.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(!objLoadingContent.activated, "Test 19c, Plugin should not be activated");
@@ -479,17 +495,17 @@ function test19c() {
 
   var text = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgClickToPlay");
   EventUtils.synthesizeMouseAtCenter(text, {}, gTestBrowser.contentWindow);
   let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed;
   waitForCondition(condition, test19d, "Test 19c, Waited too long for doorhanger to activate");
 }
 
 function test19d() {
-  prepareTest(test19e, gTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test19e), gTestRoot + "plugin_test.html");
 }
 
 // Tests that clicking the box of the overlay activates the doorhanger
 // (just to be thorough)
 function test19e() {
   var doc = gTestBrowser.contentDocument;
   var plugin = doc.getElementById("test");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
@@ -498,17 +514,17 @@ function test19e() {
   ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Test 19e, Doorhanger should start out dismissed");
 
   EventUtils.synthesizeMouse(plugin, 50, 50, {}, gTestBrowser.contentWindow);
   let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed;
   waitForCondition(condition, test19f, "Test 19e, Waited too long for plugin to activate");
 }
 
 function test19f() {
-  prepareTest(test20a, gTestRoot + "plugin_hidden_to_visible.html");
+  prepareTest(runAfterPluginBindingAttached(test20a), gTestRoot + "plugin_hidden_to_visible.html");
 }
 
 // Tests that a plugin in a div that goes from style="display: none" to
 // "display: block" can be clicked to activate.
 function test20a() {
   var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(!clickToPlayNotification, "Test 20a, Should not have a click-to-play notification");
   var doc = gTestBrowser.contentDocument;
@@ -563,17 +579,17 @@ function test20d() {
   var pluginRect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect();
   ok(pluginRect.width == 0, "Test 20d, plugin should have click-to-play overlay with zero width");
   ok(pluginRect.height == 0, "Test 20d, plugin should have click-to-play overlay with zero height");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(objLoadingContent.activated, "Test 20d, plugin should be activated");
 
   clearAllPluginPermissions();
 
-  prepareTest(test21a, gTestRoot + "plugin_two_types.html");
+  prepareTest(runAfterPluginBindingAttached(test21a), gTestRoot + "plugin_two_types.html");
 }
 
 // Test having multiple different types of plugin on one page
 function test21a() {
   var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(notification, "Test 21a, Should have a click-to-play notification");
 
   var doc = gTestBrowser.contentDocument;
@@ -691,17 +707,17 @@ function test21e() {
     ok(objLoadingContent.activated, "Test 21e, Plugin with id=" + plugin.id + " should be activated");
   }
 
   getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
   getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
 
   clearAllPluginPermissions();
 
-  prepareTest(test22, gTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test22), gTestRoot + "plugin_test.html");
 }
 
 // Tests that a click-to-play plugin retains its activated state upon reloading
 function test22() {
   ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "Test 22, Should have a click-to-play notification");
 
   // Plugin should start as CTP
   var pluginNode = gTestBrowser.contentDocument.getElementById("test");
@@ -723,17 +739,17 @@ function test22() {
   var pluginsDiffer;
   try {
     pluginNode.checkObjectValue(oldVal);
   } catch (e) {
     pluginsDiffer = true;
   }
   ok(pluginsDiffer, "Test 22, plugin should have reloaded");
 
-  prepareTest(test23, gTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test23), gTestRoot + "plugin_test.html");
 }
 
 // Tests that a click-to-play plugin resets its activated state when changing types
 function test23() {
   ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "Test 23, Should have a click-to-play notification");
 
   // Plugin should start as CTP
   var pluginNode = gTestBrowser.contentDocument.getElementById("test");
@@ -753,17 +769,17 @@ function test23() {
   pluginNode.parentNode.appendChild(pluginNode);
   is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_NULL, "Test 23, plugin should be unloaded");
   pluginNode.type = "application/x-test";
   pluginNode.parentNode.appendChild(pluginNode);
   is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_NULL, "Test 23, Plugin should not have activated");
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 23, Plugin should be click-to-play");
   ok(!pluginNode.activated, "Test 23, plugin node should not be activated");
 
-  prepareTest(test24a, gHttpTestRoot + "plugin_test.html");
+  prepareTest(runAfterPluginBindingAttached(test24a), gHttpTestRoot + "plugin_test.html");
 }
 
 // Test that "always allow"-ing a plugin will not allow it when it becomes
 // blocklisted.
 function test24a() {
   var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(notification, "Test 24a, Should have a click-to-play notification");
   var plugin = gTestBrowser.contentDocument.getElementById("test");
@@ -781,17 +797,17 @@ function test24a() {
 // did the "always allow" work as intended?
 function test24b() {
   var plugin = gTestBrowser.contentDocument.getElementById("test");
   ok(plugin, "Test 24b, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   ok(objLoadingContent.activated, "Test 24b, plugin should be activated");
   setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml",
   function() {
-    prepareTest(test24c, gHttpTestRoot + "plugin_test.html");
+    prepareTest(runAfterPluginBindingAttached(test24c), gHttpTestRoot + "plugin_test.html");
   });
 }
 
 // the plugin is now blocklisted, so it should not automatically load
 function test24c() {
   var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
   ok(notification, "Test 24c, Should have a click-to-play notification");
   var plugin = gTestBrowser.contentDocument.getElementById("test");
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -460,19 +460,21 @@ function synthesizeNativeMouseMove(aElem
  * @param aOffsetY The top offset that is added to the position (optional).
  */
 function synthesizeNativeMouseEvent(aElement, aMsg, aOffsetX = 0, aOffsetY = 0) {
   let rect = aElement.getBoundingClientRect();
   let win = aElement.ownerDocument.defaultView;
   let x = aOffsetX + win.mozInnerScreenX + rect.left + rect.width / 2;
   let y = aOffsetY + win.mozInnerScreenY + rect.top + rect.height / 2;
 
-  win.QueryInterface(Ci.nsIInterfaceRequestor)
-     .getInterface(Ci.nsIDOMWindowUtils)
-     .sendNativeMouseEvent(x, y, aMsg, 0, null);
+  let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIDOMWindowUtils);
+
+  let scale = utils.screenPixelsPerCSSPixel;
+  utils.sendNativeMouseEvent(x * scale, y * scale, aMsg, 0, null);
 }
 
 /**
  * Sends a custom drag event to a given DOM element.
  * @param aEventType The drag event's type.
  * @param aTarget The DOM element that the event is dispatched to.
  * @param aData The event's drag data (optional).
  */
--- a/browser/components/downloads/src/DownloadsCommon.jsm
+++ b/browser/components/downloads/src/DownloadsCommon.jsm
@@ -46,18 +46,22 @@ const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
                                   "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
                                   "resource:///modules/DownloadsLogger.jsm");
@@ -577,16 +581,30 @@ XPCOMUtils.defineLazyGetter(DownloadsCom
   let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
   if (os != "WINNT") {
     return false;
   }
   let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
   return parseFloat(sysInfo.getProperty("version")) >= 6;
 });
 
+/**
+ * Returns true if we should hook the panel to the JavaScript API for downloads
+ * instead of the nsIDownloadManager back-end.  In order for the logic to work
+ * properly, this value never changes during the execution of the application,
+ * even if the underlying preference value has changed.  A restart is required
+ * for the change to take effect.
+ */
+XPCOMUtils.defineLazyGetter(DownloadsCommon, "useJSTransfer", function () {
+  try {
+    return Services.prefs.getBoolPref("browser.download.useJSTransfer");
+  } catch (ex) { }
+  return false;
+});
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsData
 
 /**
  * Retrieves the list of past and completed downloads from the underlying
  * Download Manager data, and provides asynchronous notifications allowing to
  * build a consistent view of the available data.
  *
@@ -612,48 +630,143 @@ function DownloadsDataCtor(aPrivate) {
   // been removed from the Download Manager data are still present, however the
   // associated objects are replaced with the value "null".  This is required to
   // prevent race conditions when populating the list asynchronously.
   this.dataItems = {};
 
   // Array of view objects that should be notified when the available download
   // data changes.
   this._views = [];
+
+  if (DownloadsCommon.useJSTransfer) {
+    // Maps Download objects to DownloadDataItem objects.
+    this._downloadToDataItemMap = new Map();
+  }
 }
 
 DownloadsDataCtor.prototype = {
   /**
    * Starts receiving events for current downloads.
    *
    * @param aDownloadManagerService
    *        Reference to the service implementing nsIDownloadManager.  We need
    *        this because getService isn't available for us when this method is
    *        called, and we must ensure to register our listeners before the
    *        getService call for the Download Manager returns.
    */
   initializeDataLink: function DD_initializeDataLink(aDownloadManagerService)
   {
     // Start receiving real-time events.
-    aDownloadManagerService.addPrivacyAwareListener(this);
-    Services.obs.addObserver(this, "download-manager-remove-download-guid", false);
+    if (DownloadsCommon.useJSTransfer) {
+      let promiseList = this._isPrivate ? Downloads.getPrivateDownloadList()
+                                        : Downloads.getPublicDownloadList();
+      promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
+    } else {
+      aDownloadManagerService.addPrivacyAwareListener(this);
+      Services.obs.addObserver(this, "download-manager-remove-download-guid",
+                               false);
+    }
   },
 
   /**
    * Stops receiving events for current downloads and cancels any pending read.
    */
   terminateDataLink: function DD_terminateDataLink()
   {
+    if (DownloadsCommon.useJSTransfer) {
+      Cu.reportError("terminateDataLink not applicable with useJSTransfer");
+      return;
+    }
+
     this._terminateDataAccess();
 
     // Stop receiving real-time events.
     Services.obs.removeObserver(this, "download-manager-remove-download-guid");
     Services.downloads.removeListener(this);
   },
 
   //////////////////////////////////////////////////////////////////////////////
+  //// Integration with the asynchronous Downloads back-end
+
+  onDownloadAdded: function (aDownload)
+  {
+    let dataItem = new DownloadsDataItem(aDownload);
+    this._downloadToDataItemMap.set(aDownload, dataItem);
+    this.dataItems[dataItem.downloadGuid] = dataItem;
+
+    for (let view of this._views) {
+      view.onDataItemAdded(dataItem, true);
+    }
+
+    this._updateDataItemState(dataItem);
+  },
+
+  onDownloadChanged: function (aDownload)
+  {
+    let dataItem = this._downloadToDataItemMap.get(aDownload);
+    if (!dataItem) {
+      Cu.reportError("Download doesn't exist.");
+      return;
+    }
+
+    this._updateDataItemState(dataItem);
+  },
+
+  onDownloadRemoved: function (aDownload)
+  {
+    let dataItem = this._downloadToDataItemMap.get(aDownload);
+    if (!dataItem) {
+      Cu.reportError("Download doesn't exist.");
+      return;
+    }
+
+    this._downloadToDataItemMap.remove(aDownload);
+    this.dataItems[dataItem.downloadGuid] = null;
+    for (let view of this._views) {
+      view.onDataItemRemoved(dataItem);
+    }
+  },
+
+  /**
+   * Updates the given data item and sends related notifications.
+   */
+  _updateDataItemState: function (aDataItem)
+  {
+    let wasInProgress = aDataItem.inProgress;
+    let wasDone = aDataItem.done;
+
+    aDataItem.updateFromJSDownload();
+
+    if (wasInProgress && !aDataItem.inProgress) {
+      aDataItem.endTime = Date.now();
+    }
+
+    for (let view of this._views) {
+      try {
+        view.getViewItem(aDataItem).onStateChange({});
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+
+    if (!aDataItem.newDownloadNotified) {
+      aDataItem.newDownloadNotified = true;
+      this._notifyDownloadEvent("start");
+    }
+
+    if (!wasDone && aDataItem.done) {
+      this._notifyDownloadEvent("finish");
+    }
+
+    for (let view of this._views) {
+      view.getViewItem(aDataItem).onProgressChange();
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
   //// Registration of views
 
   /**
    * Adds an object to be notified when the available download data changes.
    * The specified object is initialized with the currently available downloads.
    *
    * @param aView
    *        DownloadsView object to be added.  This reference must be passed to
@@ -1155,29 +1268,92 @@ XPCOMUtils.defineLazyGetter(this, "Downl
 /**
  * Represents a single item in the list of downloads.  This object either wraps
  * an existing nsIDownload from the Download Manager, or provides the same
  * information read directly from the downloads database, with the possibility
  * of querying the nsIDownload lazily, for performance reasons.
  *
  * @param aSource
  *        Object containing the data with which the item should be initialized.
- *        This should implement either nsIDownload or mozIStorageRow.
+ *        This should implement either nsIDownload or mozIStorageRow.  If the
+ *        JavaScript API for downloads is enabled, this is a Download object.
  */
 function DownloadsDataItem(aSource)
 {
-  if (aSource instanceof Ci.nsIDownload) {
+  if (DownloadsCommon.useJSTransfer) {
+    this._initFromJSDownload(aSource);
+  } else if (aSource instanceof Ci.nsIDownload) {
     this._initFromDownload(aSource);
   } else {
     this._initFromDataRow(aSource);
   }
 }
 
 DownloadsDataItem.prototype = {
   /**
+   * The JavaScript API does not need identifiers for Download objects, so they
+   * are generated sequentially for the corresponding DownloadDataItem.
+   */
+  get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
+  __lastId: 0,
+
+  /**
+   * Initializes this object from the JavaScript API for downloads.
+   *
+   * The endTime property is initialized to the current date and time.
+   *
+   * @param aDownload
+   *        The Download object with the current state.
+   */
+  _initFromJSDownload: function (aDownload)
+  {
+    this._download = aDownload;
+
+    this.downloadGuid = "id:" + this._autoIncrementId;
+    this.file = aDownload.target.path;
+    this.target = OS.Path.basename(aDownload.target.path);
+    this.uri = aDownload.source.url;
+    this.endTime = Date.now();
+
+    this.updateFromJSDownload();
+  },
+
+  /**
+   * Updates this object from the JavaScript API for downloads.
+   */
+  updateFromJSDownload: function ()
+  {
+    // Collapse state using the correct priority.
+    if (this._download.succeeded) {
+      this.state = nsIDM.DOWNLOAD_FINISHED;
+    } else if (this._download.error &&
+               this._download.error.becauseBlockedByParentalControls) {
+      this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
+    } else if (this._download.error) {
+      this.state = nsIDM.DOWNLOAD_FAILED;
+    } else if (this._download.canceled && this._download.hasPartialData) {
+      this.state = nsIDM.DOWNLOAD_PAUSED;
+    } else if (this._download.canceled) {
+      this.state = nsIDM.DOWNLOAD_CANCELED;
+    } else if (this._download.stopped) {
+      this.state = nsIDM.DOWNLOAD_NOTSTARTED;
+    } else {
+      this.state = nsIDM.DOWNLOAD_DOWNLOADING;
+    }
+
+    this.referrer = this._download.source.referrer;
+    this.startTime = this._download.startTime;
+    this.currBytes = this._download.currentBytes;
+    this.maxBytes = this._download.totalBytes;
+    this.resumable = this._download.hasPartialData;
+    this.speed = 0;
+    this.percentComplete = this._download.progress;
+  },
+
+  /**
    * Initializes this object from a download object of the Download Manager.
    *
    * The endTime property is initialized to the current date and time.
    *
    * @param aDownload
    *        The nsIDownload with the current state.
    */
   _initFromDownload: function DDI_initFromDownload(aDownload)
@@ -1403,16 +1579,21 @@ DownloadsDataItem.prototype = {
   /**
    * Open the target file for this download.
    *
    * @param aOwnerWindow
    *        The window with which the required action is associated.
    * @throws if the file cannot be opened.
    */
   openLocalFile: function DDI_openLocalFile(aOwnerWindow) {
+    if (DownloadsCommon.useJSTransfer) {
+      this._download.launch().then(null, Cu.reportError);
+      return;
+    }
+
     this.getDownload(function(aDownload) {
       DownloadsCommon.openDownloadedFile(this.localFile,
                                          aDownload.MIMEInfo,
                                          aOwnerWindow);
     }.bind(this));
   },
 
   /**
@@ -1422,16 +1603,25 @@ DownloadsDataItem.prototype = {
     DownloadsCommon.showDownloadedFile(this.localFile);
   },
 
   /**
    * Resumes the download if paused, pauses it if active.
    * @throws if the download is not resumable or if has already done.
    */
   togglePauseResume: function DDI_togglePauseResume() {
+    if (DownloadsCommon.useJSTransfer) {
+      if (this._download.stopped) {
+        this._download.start();
+      } else {
+        this._download.cancel();
+      }
+      return;
+    }
+
     if (!this.inProgress || !this.resumable)
       throw new Error("The given download cannot be paused or resumed");
 
     this.getDownload(function(aDownload) {
       if (this.inProgress) {
         if (this.paused)
           aDownload.resume();
         else
@@ -1440,18 +1630,23 @@ DownloadsDataItem.prototype = {
     }.bind(this));
   },
 
   /**
    * Attempts to retry the download.
    * @throws if we cannot.
    */
   retry: function DDI_retry() {
+    if (DownloadsCommon.useJSTransfer) {
+      this._download.start();
+      return;
+    }
+
     if (!this.canRetry)
-      throw new Error("Cannot rerty this download");
+      throw new Error("Cannot retry this download");
 
     this.getDownload(function(aDownload) {
       aDownload.retry();
     });
   },
 
   /**
    * Support function that deletes the local file for a download. This is
@@ -1468,29 +1663,45 @@ DownloadsDataItem.prototype = {
     } catch (ex) { }
   },
 
   /**
    * Cancels the download.
    * @throws if the download is already done.
    */
   cancel: function() {
+    if (DownloadsCommon.useJSTransfer) {
+      this._download.cancel();
+      this._download.removePartialData().then(null, Cu.reportError);
+      return;
+    }
+
     if (!this.inProgress)
       throw new Error("Cannot cancel this download");
 
     this.getDownload(function (aDownload) {
       aDownload.cancel();
       this._ensureLocalFileRemoved();
     }.bind(this));
   },
 
   /**
    * Remove the download.
    */
   remove: function DDI_remove() {
+    if (DownloadsCommon.useJSTransfer) {
+      let promiseList = this._download.source.isPrivate
+                          ? Downloads.getPrivateDownloadList()
+                          : Downloads.getPublicDownloadList();
+      promiseList.then(list => list.remove(this._download))
+                 .then(() => this._download.finalize(true))
+                 .then(null, Cu.reportError);
+      return;
+    }
+
     this.getDownload(function (aDownload) {
       if (this.inProgress) {
         aDownload.cancel();
         this._ensureLocalFileRemoved();
       }
       aDownload.remove();
     }.bind(this));
   }
--- a/browser/components/downloads/src/DownloadsStartup.js
+++ b/browser/components/downloads/src/DownloadsStartup.js
@@ -81,43 +81,45 @@ DownloadsStartup.prototype = {
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function DS_observe(aSubject, aTopic, aData)
   {
     switch (aTopic) {
       case "profile-after-change":
-        kObservedTopics.forEach(
-          function (topic) Services.obs.addObserver(this, topic, true),
-          this);
-
         // Override Toolkit's nsIDownloadManagerUI implementation with our own.
         // This must be done at application startup and not in the manifest to
         // ensure that our implementation overrides the original one.
         Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
                           .registerFactory(kDownloadsUICid, "",
                                            kDownloadsUIContractId, null);
 
         // If the integration preference is enabled, override Toolkit's
         // nsITransfer implementation with the one from the JavaScript API for
         // downloads.  This should be used only by developers while testing new
         // code that uses the JavaScript API, and will eventually be removed
         // when nsIDownloadManager will not be available anymore (bug 851471).
         let useJSTransfer = false;
         try {
+          // For performance reasons, we don't want to load the DownloadsCommon
+          // module during startup, so we read the preference value directly.
           useJSTransfer =
             Services.prefs.getBoolPref("browser.download.useJSTransfer");
-        } catch (ex) {
-          // This is a hidden preference that does not exist by default.
-        }
+        } catch (ex) { }
         if (useJSTransfer) {
           Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
                             .registerFactory(kTransferCid, "",
                                              kTransferContractId, null);
+        } else {
+          // The other notifications are handled internally by the JavaScript
+          // API for downloads, no need to observe when that API is enabled.
+          for (let topic of kObservedTopics) {
+            Services.obs.addObserver(this, topic, true);
+          }
         }
         break;
 
       case "sessionstore-windows-restored":
       case "sessionstore-browser-state-restored":
         // Unless there is no saved session, there is a chance that we are
         // starting up after a restart or a crash.  We should check the disk
         // database to see if there are completed downloads to recover and show
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -9,16 +9,19 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/SignInToWebsite.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
+                                  "resource:///modules/AboutHome.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
@@ -457,16 +460,17 @@ BrowserGlue.prototype = {
 
     webappsUI.init();
     PageThumbs.init();
     NewTabUtils.init();
     BrowserNewTabPreloader.init();
     SignInToWebsiteUX.init();
     PdfJs.init();
     webrtcUI.init();
+    AboutHome.init();
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
   },
 
   _checkForOldBuildUpdates: function () {
     // check for update if our build is old
     if (Services.prefs.getBoolPref("app.update.enabled") &&
         Services.prefs.getBoolPref("app.update.checkInstallTime")) {
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -107,18 +107,16 @@ let gDocShellCapabilities = (function ()
       let keys = Object.keys(docShell);
       caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5));
     }
 
     return caps;
   };
 })();
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
   "resource:///modules/devtools/scratchpad-manager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
   "resource:///modules/sessionstore/DocumentUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
   "resource:///modules/sessionstore/_SessionFile.jsm");
@@ -308,19 +306,16 @@ let SessionStoreInternal = {
 
   // internal states for all open windows (data we need to associate,
   // but not write to disk)
   _internalWindows: {},
 
   // states for all recently closed windows
   _closedWindows: [],
 
-  // not-"dirty" windows usually don't need to have their data updated
-  _dirtyWindows: {},
-
   // collection of session states yet to be restored
   _statesToRestore: {},
 
   // counts the number of crashes since the last clean start
   _recentCrashes: 0,
 
   // whether the last window was closed and should be restored
   _restoreLastWindow: false,
@@ -344,26 +339,16 @@ let SessionStoreInternal = {
 
   // Whether session has been initialized
   _sessionInitialized: false,
 
   // True if session store is disabled by multi-process browsing.
   // See bug 516755.
   _disabledForMultiProcess: false,
 
-  // The original "sessionstore.resume_session_once" preference value before it
-  // was modified by saveState.  saveState will set the
-  // "sessionstore.resume_session_once" to true when the
-  // the "sessionstore.resume_from_crash" preference is false (crash recovery
-  // is disabled) so that pinned tabs will be restored in the case of a
-  // crash.  This variable is used to restore the original value so the
-  // previous session is not always restored when
-  // "sessionstore.resume_from_crash" is true.
-  _resume_session_once_on_shutdown: null,
-
   /**
    * A promise fulfilled once initialization is complete.
    */
   get promiseInitialized() {
     return this._deferredInitialized.promise;
   },
 
   /* ........ Public Getters .............. */
@@ -495,18 +480,16 @@ let SessionStoreInternal = {
     }
 
     // at this point, we've as good as resumed the session, so we can
     // clear the resume_session_once flag, if it's set
     if (this._loadState != STATE_QUITTING &&
         this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
       this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
 
-    this._initEncoding();
-
     this._performUpgradeBackup();
 
     this._sessionInitialized = true;
   },
 
   /**
    * If this is the first time we launc this build of Firefox,
    * backup sessionstore.js.
@@ -531,46 +514,32 @@ let SessionStoreInternal = {
         yield _SessionFile.removeBackupCopy("-" + latestBackup);
       } catch (ex) {
         debug("Could not perform upgrade backup " + ex);
         debug(ex.stack);
       }
     }.bind(this));
   },
 
-  _initEncoding : function ssi_initEncoding() {
-    // The (UTF-8) encoder used to write to files.
-    XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () {
-      return new TextEncoder();
-    });
-  },
-
   _initPrefs : function() {
     this._prefBranch = Services.prefs.getBranch("browser.");
 
     gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
 
     Services.prefs.addObserver("browser.sessionstore.debug", () => {
       gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
     }, false);
 
     // minimal interval between two save operations (in milliseconds)
     XPCOMUtils.defineLazyGetter(this, "_interval", function () {
       // used often, so caching/observing instead of fetching on-demand
       this._prefBranch.addObserver("sessionstore.interval", this, true);
       return this._prefBranch.getIntPref("sessionstore.interval");
     });
 
-    // when crash recovery is disabled, session data is not written to disk
-    XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function () {
-      // get crash recovery state from prefs and allow for proper reaction to state changes
-      this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true);
-      return this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
-    });
-
     XPCOMUtils.defineLazyGetter(this, "_max_tabs_undo", function () {
       this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
       return this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
     });
 
     XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
       this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
       return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
@@ -1014,17 +983,17 @@ let SessionStoreInternal = {
     this._forEachBrowserWindow(function(aWindow) {
       this._collectWindowData(aWindow);
     });
     // we must cache this because _getMostRecentBrowserWindow will always
     // return null by the time quit-application occurs
     var activeWindow = this._getMostRecentBrowserWindow();
     if (activeWindow)
       this.activeWindowSSiCache = activeWindow.__SSi || "";
-    this._dirtyWindows = [];
+    DirtyWindows.clear();
   },
 
   /**
    * On quit application granted
    */
   onQuitApplicationGranted: function ssi_onQuitApplicationGranted() {
     // freeze the data at what we've got (ignoring closing windows)
     this._loadState = STATE_QUITTING;
@@ -1052,25 +1021,16 @@ let SessionStoreInternal = {
       // The browser:purge-session-history notification fires after the
       // quit-application notification so unregister the
       // browser:purge-session-history notification to prevent clearing
       // session data on disk on a restart.  It is also unnecessary to
       // perform any other sanitization processing on a restart as the
       // browser is about to exit anyway.
       Services.obs.removeObserver(this, "browser:purge-session-history");
     }
-    else if (this._resume_session_once_on_shutdown != null) {
-      // if the sessionstore.resume_session_once preference was changed by
-      // saveState because crash recovery is disabled then restore the
-      // preference back to the value it was prior to that.  This will prevent
-      // SessionStore from always restoring the session when crash recovery is
-      // disabled.
-      this._prefBranch.setBoolPref("sessionstore.resume_session_once",
-                                   this._resume_session_once_on_shutdown);
-    }
 
     if (aData != "restart") {
       // Throw away the previous session on shutdown
       this._lastSessionState = null;
     }
 
     this._loadState = STATE_QUITTING; // just to be sure
     this._uninit();
@@ -1207,30 +1167,16 @@ let SessionStoreInternal = {
         this._interval = this._prefBranch.getIntPref("sessionstore.interval");
         // reset timer and save
         if (this._saveTimer) {
           this._saveTimer.cancel();
           this._saveTimer = null;
         }
         this.saveStateDelayed(null, -1);
         break;
-      case "sessionstore.resume_from_crash":
-        this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
-        // restore original resume_session_once preference if set in saveState
-        if (this._resume_session_once_on_shutdown != null) {
-          this._prefBranch.setBoolPref("sessionstore.resume_session_once",
-                                       this._resume_session_once_on_shutdown);
-          this._resume_session_once_on_shutdown = null;
-        }
-        // either create the file with crash recovery information or remove it
-        // (when _loadState is not STATE_RUNNING, that file is used for session resuming instead)
-        if (!this._resume_from_crash)
-          _SessionFile.wipe();
-        this.saveState(true);
-        break;
     }
   },
 
   /**
    * On timer callback
    */
   onTimerCallback: function ssi_onTimerCallback() {
     this._saveTimer = null;
@@ -2569,38 +2515,36 @@ let SessionStoreInternal = {
     else if (winData.sidebar)
       delete winData.sidebar;
   },
 
   /**
    * gather session data as object
    * @param aUpdateAll
    *        Bool update all windows
-   * @param aPinnedOnly
-   *        Bool collect pinned tabs only
    * @returns object
    */
-  _getCurrentState: function ssi_getCurrentState(aUpdateAll, aPinnedOnly) {
+  _getCurrentState: function ssi_getCurrentState(aUpdateAll) {
     this._handleClosedWindows();
 
     var activeWindow = this._getMostRecentBrowserWindow();
 
     if (this._loadState == STATE_RUNNING) {
       // update the data for all windows with activities since the last save operation
       this._forEachBrowserWindow(function(aWindow) {
         if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
           return;
-        if (aUpdateAll || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) {
+        if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
           this._collectWindowData(aWindow);
         }
         else { // always update the window features (whose change alone never triggers a save operation)
           this._updateWindowFeatures(aWindow);
         }
       });
-      this._dirtyWindows = [];
+      DirtyWindows.clear();
     }
 
     // collect the data for all windows
     var total = [], windows = {}, ids = [];
     var nonPopupCount = 0;
     var ix;
     for (ix in this._windows) {
       if (this._windows[ix]._restoring) // window data is still in _statesToRestore
@@ -2636,34 +2580,16 @@ let SessionStoreInternal = {
       // prepend the last non-popup browser window, so that if the user loads more tabs
       // at startup we don't accidentally add them to a popup window
       do {
         total.unshift(lastClosedWindowsCopy.shift())
       } while (total[0].isPopup && lastClosedWindowsCopy.length > 0)
     }
 #endif
 
-    if (aPinnedOnly) {
-      // perform a deep copy so that existing session variables are not changed.
-      total = JSON.parse(this._toJSONString(total));
-      total = total.filter(function (win) {
-        win.tabs = win.tabs.filter(function (tab) tab.pinned);
-        // remove closed tabs
-        win._closedTabs = [];
-        // correct selected tab index if it was stripped out
-        if (win.selected > win.tabs.length)
-          win.selected = 1;
-        return win.tabs.length > 0;
-      });
-      if (total.length == 0)
-        return null;
-
-      lastClosedWindowsCopy = [];
-    }
-
     if (activeWindow) {
       this.activeWindowSSiCache = activeWindow.__SSi || "";
     }
     ix = ids.indexOf(this.activeWindowSSiCache);
     // We don't want to restore focus to a minimized window or a window which had all its
     // tabs stripped out (doesn't exist).
     if (ix != -1 && total[ix] && total[ix].sizemode == "minimized")
       ix = -1;
@@ -2739,17 +2665,17 @@ let SessionStoreInternal = {
     this._updateWindowFeatures(aWindow);
 
     // Make sure we keep __SS_lastSessionWindowID around for cases like entering
     // or leaving PB mode.
     if (aWindow.__SS_lastSessionWindowID)
       this._windows[aWindow.__SSi].__lastSessionWindowID =
         aWindow.__SS_lastSessionWindowID;
 
-    this._dirtyWindows[aWindow.__SSi] = false;
+    DirtyWindows.remove(aWindow);
   },
 
   /* ........ Restoring Functionality .............. */
 
   /**
    * restore features to a single window
    * @param aWindow
    *        Window reference
@@ -3064,16 +2990,20 @@ let SessionStoreInternal = {
       }
     }
 
     if (!this._isWindowLoaded(aWindow)) {
       // from now on, the data will come from the actual window
       delete this._statesToRestore[aWindow.__SS_restoreID];
       delete aWindow.__SS_restoreID;
       delete this._windows[aWindow.__SSi]._restoring;
+
+      // It's important to set the window state to dirty so that
+      // we collect their data for the first time when saving state.
+      DirtyWindows.add(aWindow);
     }
 
     if (aTabs.length == 0) {
       // this is normally done in restoreHistory() but as we're returning early
       // here we need to take care of it.
       this._setWindowStateReady(aWindow);
       return;
     }
@@ -3754,17 +3684,17 @@ let SessionStoreInternal = {
    * marks window as dirty (i.e. data update can't be skipped)
    * @param aWindow
    *        Window reference
    * @param aDelay
    *        Milliseconds to delay
    */
   saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) {
     if (aWindow) {
-      this._dirtyWindows[aWindow.__SSi] = true;
+      DirtyWindows.add(aWindow);
     }
 
     if (!this._saveTimer) {
       // interval until the next disk operation is allowed
       var minimalDelay = this._lastSaveTime + this._interval - Date.now();
 
       // if we have to wait, set a timer, otherwise saveState directly
       aDelay = Math.max(minimalDelay, aDelay);
@@ -3781,22 +3711,20 @@ let SessionStoreInternal = {
   /**
    * save state to disk
    * @param aUpdateAll
    *        Bool update all windows
    */
   saveState: function ssi_saveState(aUpdateAll) {
     // If crash recovery is disabled, we only want to resume with pinned tabs
     // if we crash.
-    let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash;
-
     TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_DATA_MS");
     TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS");
 
-    var oState = this._getCurrentState(aUpdateAll, pinnedOnly);
+    var oState = this._getCurrentState(aUpdateAll);
     if (!oState) {
       TelemetryStopwatch.cancel("FX_SESSION_RESTORE_COLLECT_DATA_MS");
       TelemetryStopwatch.cancel("FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS");
       return;
     }
 
     // Forget about private windows.
     for (let i = oState.windows.length - 1; i >= 0; i--) {
@@ -3836,29 +3764,16 @@ let SessionStoreInternal = {
       }
       else {
         // We only need to go until we hit !needsRestore since we're going in reverse
         break;
       }
     }
 #endif
 
-    if (pinnedOnly) {
-      // Save original resume_session_once preference for when quiting browser,
-      // otherwise session will be restored next time browser starts and we
-      // only want it to be restored in the case of a crash.
-      if (this._resume_session_once_on_shutdown == null) {
-        this._resume_session_once_on_shutdown =
-          this._prefBranch.getBoolPref("sessionstore.resume_session_once");
-        this._prefBranch.setBoolPref("sessionstore.resume_session_once", true);
-        // flush the preference file so preference will be saved in case of a crash
-        Services.prefs.savePrefFile(null);
-      }
-    }
-
     // Persist the last session if we deferred restoring it
     if (this._lastSessionState)
       oState.lastSessionState = this._lastSessionState;
 
     TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_DATA_MS");
     TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS");
 
     this._saveStateObject(oState);
@@ -3879,18 +3794,17 @@ let SessionStoreInternal = {
     data = stateString.data;
 
     // Don't touch the file if an observer has deleted all state data.
     if (!data) {
       return;
     }
 
     // Write (atomically) to a session file, using a tmp file.
-    let promise =
-      _SessionFile.write(data, {backupOnFirstWrite: this._resume_from_crash});
+    let promise = _SessionFile.write(data);
 
     // Once the session file is successfully updated, save the time stamp of the
     // last save and notify the observers.
     promise = promise.then(() => {
       this._lastSaveTime = Date.now();
       Services.obs.notifyObservers(null, "sessionstore-state-write-complete",
         "");
     });
@@ -4758,16 +4672,38 @@ let DyingWindowCache = {
     this._data.set(window, data);
   },
 
   remove: function (window) {
     this._data.delete(window);
   }
 };
 
+// A weak set of dirty windows. We use it to determine which windows we need to
+// recollect data for when _getCurrentState() is called.
+let DirtyWindows = {
+  _data: new WeakMap(),
+
+  has: function (window) {
+    return this._data.has(window);
+  },
+
+  add: function (window) {
+    return this._data.set(window, true);
+  },
+
+  remove: function (window) {
+    this._data.delete(window);
+  },
+
+  clear: function (window) {
+    this._data.clear();
+  }
+};
+
 // A map storing the number of tabs last closed per windoow. This only
 // stores the most recent tab-close operation, and is used to undo
 // batch tab-closing operations.
 let NumberOfTabsClosedLastPerWindow = new WeakMap();
 
 // A set of tab attributes to persist. We will read a given list of tab
 // attributes when collecting tab data and will re-set those attributes when
 // the given tab data is restored to a new tab.
--- a/browser/components/sessionstore/src/SessionWorker.js
+++ b/browser/components/sessionstore/src/SessionWorker.js
@@ -119,33 +119,43 @@ let Agent = {
     }
     // No sessionstore data files found. Return an empty string.
     return {result: ""};
   },
 
   /**
    * Write the session to disk.
    */
-  write: function (stateString, options) {
+  write: function (stateString) {
+    let exn;
     let telemetry = {};
+
     if (!this.hasWrittenState) {
-      if (options && options.backupOnFirstWrite) {
-        try {
-          let startMs = Date.now();
-          File.move(this.path, this.backupPath);
-          telemetry.FX_SESSION_RESTORE_BACKUP_FILE_MS = Date.now() - startMs;
-        } catch (ex if isNoSuchFileEx(ex)) {
-          // Ignore exceptions about non-existent files.
-        }
+      try {
+        let startMs = Date.now();
+        File.move(this.path, this.backupPath);
+        telemetry.FX_SESSION_RESTORE_BACKUP_FILE_MS = Date.now() - startMs;
+      } catch (ex if isNoSuchFileEx(ex)) {
+        // Ignore exceptions about non-existent files.
+      } catch (ex) {
+        // Throw the exception after we wrote the state to disk
+        // so that the backup can't interfere with the actual write.
+        exn = ex;
       }
 
       this.hasWrittenState = true;
     }
 
-    return this._write(stateString, telemetry);
+    let ret = this._write(stateString, telemetry);
+
+    if (exn) {
+      throw exn;
+    }
+
+    return ret;
   },
 
   /**
    * Writes the session state to disk again but changes session.state to
    * 'running' before doing so. This is intended to be called only once, shortly
    * after startup so that we detect crashes on startup correctly.
    */
   writeLoadStateOnceAfterStartup: function (loadState) {
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -62,18 +62,18 @@ this._SessionFile = {
     Deprecated.warning(
       "syncRead is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=532150")
     return SessionFileInternal.syncRead();
   },
   /**
    * Write the contents of the session file, asynchronously.
    */
-  write: function (aData, aOptions = {}) {
-    return SessionFileInternal.write(aData, aOptions);
+  write: function (aData) {
+    return SessionFileInternal.write(aData);
   },
   /**
    * Writes the initial state to disk again only to change the session's load
    * state. This must only be called once, it will throw an error otherwise.
    */
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     return SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
   },
@@ -204,23 +204,23 @@ let SessionFileInternal = {
 
   read: function () {
     return SessionWorker.post("read").then(msg => {
       this._recordTelemetry(msg.telemetry);
       return msg.ok;
     });
   },
 
-  write: function (aData, aOptions) {
+  write: function (aData) {
     let refObj = {};
     return TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       try {
-        let promise = SessionWorker.post("write", [aData, aOptions]);
+        let promise = SessionWorker.post("write", [aData]);
         // At this point, we measure how long we stop the main thread
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
         // Now wait for the result and record how long the write took
         let msg = yield promise;
         this._recordTelemetry(msg.telemetry);
       } catch (ex) {
         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -117,16 +117,18 @@ MOCHITEST_BROWSER_FILES = \
 	browser_615394-SSWindowState_events.js \
 	browser_618151.js \
 	browser_623779.js \
 	browser_624727.js \
 	browser_625257.js \
 	browser_628270.js \
 	browser_635418.js \
 	browser_636279.js \
+	browser_637020.js \
+	browser_637020_slow.sjs \
 	browser_644409-scratchpads.js \
 	browser_645428.js \
 	browser_659591.js \
 	browser_662743.js \
 	browser_662743_sample.html \
 	browser_662812.js \
 	browser_665702-state_session.js \
 	browser_682507.js \
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_637020.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/components/" +
+                 "sessionstore/test/browser_637020_slow.sjs";
+
+const TEST_STATE = {
+  windows: [{
+    tabs: [
+      { entries: [{ url: "about:mozilla" }] },
+      { entries: [{ url: "about:robots" }] }
+    ]
+  }, {
+    tabs: [
+      { entries: [{ url: TEST_URL }] },
+      { entries: [{ url: TEST_URL }] }
+    ]
+  }]
+};
+
+function test() {
+  TestRunner.run();
+}
+
+/**
+ * This test ensures that windows that have just been restored will be marked
+ * as dirty, otherwise _getCurrentState() might ignore them when collecting
+ * state for the first time and we'd just save them as empty objects.
+ *
+ * The dirty state acts as a cache to not collect data from all windows all the
+ * time, so at the beginning, each window must be dirty so that we collect
+ * their state at least once.
+ */
+
+function runTests() {
+  let win;
+
+  // Wait until the new window has been opened.
+  Services.obs.addObserver(function onOpened(subject) {
+    Services.obs.removeObserver(onOpened, "domwindowopened");
+    win = subject;
+    executeSoon(next);
+  }, "domwindowopened", false);
+
+  // Set the new browser state that will
+  // restore a window with two slowly loading tabs.
+  yield SessionStore.setBrowserState(JSON.stringify(TEST_STATE));
+
+  // The window has now been opened. Check the state that is returned,
+  // this should come from the cache while the window isn't restored, yet.
+  info("the window has been opened");
+  checkWindows();
+
+  // The history has now been restored and the tabs are loading. The data must
+  // now come from the window, if it's correctly been marked as dirty before.
+  yield whenDelayedStartupFinished(win, next);
+  info("the delayed startup has finished");
+  checkWindows();
+}
+
+function checkWindows() {
+  let state = JSON.parse(SessionStore.getBrowserState());
+  is(state.windows[0].tabs.length, 2, "first window has two tabs");
+  is(state.windows[1].tabs.length, 2, "second window has two tabs");
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_637020_slow.sjs
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const DELAY_MS = "2000";
+
+let timer;
+
+function handleRequest(req, resp) {
+  resp.processAsync();
+  resp.setHeader("Cache-Control", "no-cache", false);
+  resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+  timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.init(() => {
+    resp.write("hi");
+    resp.finish();
+  }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
--- a/browser/components/sessionstore/test/unit/test_backup_once.js
+++ b/browser/components/sessionstore/test/unit/test_backup_once.js
@@ -20,29 +20,29 @@ let pathBackup;
 let decoder;
 
 // Write to the store, and check that a backup is created first
 add_task(function test_first_write_backup() {
   let content = "test_1";
   let initial_content = decoder.decode(yield OS.File.read(pathStore));
 
   do_check_true(!(yield OS.File.exists(pathBackup)));
-  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+  yield _SessionFile.write(content);
   do_check_true(yield OS.File.exists(pathBackup));
 
   let backup_content = decoder.decode(yield OS.File.read(pathBackup));
   do_check_eq(initial_content, backup_content);
 });
 
 // Write to the store again, and check that the backup is not updated
 add_task(function test_second_write_no_backup() {
   let content = "test_2";
   let initial_content = decoder.decode(yield OS.File.read(pathStore));
   let initial_backup_content = decoder.decode(yield OS.File.read(pathBackup));
 
-  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+  yield _SessionFile.write(content);
 
   let written_content = decoder.decode(yield OS.File.read(pathStore));
   do_check_eq(content, written_content);
 
   let backup_content = decoder.decode(yield OS.File.read(pathBackup));
   do_check_eq(initial_backup_content, backup_content);
 });
deleted file mode 100644
--- a/browser/components/sessionstore/test/unit/test_no_backup_first_write.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-let toplevel = this;
-Cu.import("resource://gre/modules/osfile.jsm");
-
-function run_test() {
-  let profd = do_get_profile();
-  Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", toplevel);
-  pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
-  pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
-  let source = do_get_file("data/sessionstore_valid.js");
-  source.copyTo(profd, "sessionstore.js");
-  run_next_test();
-}
-
-let pathStore;
-let pathBackup;
-
-// Write to the store first with |backupOnFirstWrite: false|,
-// and make sure second write does not backup even with
-// |backupOnFirstWrite: true|
-add_task(function test_no_backup_on_second_write() {
-  let content = "test_1";
-
-  do_check_true(!(yield OS.File.exists(pathBackup)));
-  yield _SessionFile.write(content, {backupOnFirstWrite: false});
-  do_check_true(!(yield OS.File.exists(pathBackup)));
-
-  yield _SessionFile.write(content, {backupOnFirstWrite: true});
-  do_check_true(!(yield OS.File.exists(pathBackup)));
-});
--- a/browser/components/sessionstore/test/unit/xpcshell.ini
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -1,16 +1,15 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 
 [test_backup.js]
 [test_backup_once.js]
-[test_no_backup_first_write.js]
 [test_startup_nosession_sync.js]
 # bug 845190 - thread pool wasn't shutdown assertions
 skip-if = (os == "win" || "linux") && debug
 [test_startup_nosession_async.js]
 [test_startup_session_sync.js]
 # bug 845190 - thread pool wasn't shutdown assertions
 skip-if = (os == "win" || "linux") && debug
 [test_startup_session_async.js]
--- a/browser/components/tabview/groupitems.js
+++ b/browser/components/tabview/groupitems.js
@@ -1915,17 +1915,16 @@ let GroupItems = {
   _arrangePaused: false,
   _arrangesPending: [],
   _removingHiddenGroups: false,
   _delayedModUpdates: [],
   _autoclosePaused: false,
   minGroupHeight: 110,
   minGroupWidth: 125,
   _lastActiveList: null,
-  _lastGroupToUpdateTabBar: null,
 
   // ----------
   // Function: toString
   // Prints [GroupItems] for debug use
   toString: function GroupItems_toString() {
     return "[GroupItems count=" + this.groupItems.length + "]";
   },
 
@@ -2281,20 +2280,16 @@ let GroupItems = {
     if (groupItem == this._activeGroupItem)
       this._activeGroupItem = null;
 
     this._arrangesPending = this._arrangesPending.filter(function (pending) {
       return groupItem != pending.groupItem;
     });
 
     this._lastActiveList.remove(groupItem);
-
-    if (this._lastGroupToUpdateTabBar == groupItem)
-      this._lastGroupToUpdateTabBar = null;
-
     UI.updateTabButton();
   },
 
   // ----------
   // Function: groupItem
   // Given some sort of identifier, returns the appropriate groupItem.
   // Currently only supports groupItem ids.
   groupItem: function GroupItems_groupItem(a) {
@@ -2418,23 +2413,18 @@ let GroupItems = {
   // Function: _updateTabBar
   // Hides and shows tabs in the tab bar based on the active groupItem
   _updateTabBar: function GroupItems__updateTabBar() {
     if (!window.UI)
       return; // called too soon
 
     Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
 
-    // Update list of visible tabs only once after switching to another group.
-    if (this._activeGroupItem == this._lastGroupToUpdateTabBar)
-      return;
-
     let tabItems = this._activeGroupItem._children;
     gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
-    this._lastGroupToUpdateTabBar = this._activeGroupItem;
   },
 
   // ----------
   // Function: updateActiveGroupItemAndTabBar
   // Sets active TabItem and GroupItem, and updates tab bar appropriately.
   // Parameters:
   // tabItem - the tab item
   // options - is passed to UI.setActive() directly
@@ -2542,30 +2532,30 @@ let GroupItems = {
       return;
 
     Utils.assertThrow(tab._tabViewTabItem, "tab must be linked to a TabItem");
 
     // given tab is already contained in target group
     if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId)
       return;
 
-    let shouldHideTab = false;
+    let shouldUpdateTabBar = false;
     let shouldShowTabView = false;
     let groupItem;
 
     // switch to the appropriate tab first.
     if (tab.selected) {
       if (gBrowser.visibleTabs.length > 1) {
         gBrowser._blurTab(tab);
-        shouldHideTab = true;
+        shouldUpdateTabBar = true;
       } else {
         shouldShowTabView = true;
       }
     } else {
-      shouldHideTab = true;
+      shouldUpdateTabBar = true
     }
 
     // remove tab item from a groupItem
     if (tab._tabViewTabItem.parent)
       tab._tabViewTabItem.parent.remove(tab._tabViewTabItem);
 
     // add tab item to a groupItem
     if (groupItemId) {
@@ -2578,18 +2568,18 @@ let GroupItems = {
 
       let box = new Rect(pageBounds);
       box.width = 250;
       box.height = 200;
 
       new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true });
     }
 
-    if (shouldHideTab)
-      gBrowser.hideTab(tab);
+    if (shouldUpdateTabBar)
+      this._updateTabBar();
     else if (shouldShowTabView)
       UI.showTabView();
   },
 
   // ----------
   // Function: removeHiddenGroups
   // Removes all hidden groups' data and its browser tabs.
   removeHiddenGroups: function GroupItems_removeHiddenGroups() {
--- a/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js
+++ b/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js
@@ -94,37 +94,73 @@ function test() {
             assertOneSingleGroupItem(aWindow);
             next(aWindow);
           }, aWindow);
         }, aWindow);
       }, aWindow);
     }, aWindow);
   }
 
-  function testOnWindow(aCallback) {
-    let win = OpenBrowserWindow({private: false});
+  // [624102] check state after return from private browsing
+  let testPrivateBrowsing = function (aWindow) {
+    aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#1', {inBackground: true});
+    aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#2', {inBackground: true});
+
+    let cw = getContentWindow(aWindow);
+    let box = new cw.Rect(20, 20, 250, 200);
+    let groupItem = new cw.GroupItem([], {bounds: box, immediately: true});
+    cw.UI.setActive(groupItem);
+
+    aWindow.gBrowser.selectedTab = aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#3', {inBackground: true});
+    aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#4', {inBackground: true});
+
+    afterAllTabsLoaded(function () {
+      assertNumberOfVisibleTabs(aWindow, 2);
+
+      enterAndLeavePrivateBrowsing(function () {
+        assertNumberOfVisibleTabs(aWindow, 2);
+        aWindow.gBrowser.selectedTab = aWindow.gBrowser.tabs[0];
+        closeGroupItem(cw.GroupItems.groupItems[1], function() {
+          next(aWindow);
+        });
+      });
+    }, aWindow);
+  }
+
+  function testOnWindow(aIsPrivate, aCallback) {
+    let win = OpenBrowserWindow({private: aIsPrivate});
     win.addEventListener("load", function onLoad() {
       win.removeEventListener("load", onLoad, false);
       executeSoon(function() { aCallback(win) });
     }, false);
   }
 
+  function enterAndLeavePrivateBrowsing(callback) {
+    testOnWindow(true, function (aWindow) {
+      aWindow.close();
+      callback();
+    });
+  }
+
   waitForExplicitFinish();
 
   // Tests for #624265
   tests.push(testUndoCloseTabs);
 
   // Tests for #623792
   tests.push(testDuplicateTab);
   tests.push(testBackForwardDuplicateTab);
 
-  testOnWindow(function(aWindow) {
+  // Tests for #624102
+  tests.push(testPrivateBrowsing);
+
+  testOnWindow(false, function(aWindow) {
     loadTabView(function() {
       next(aWindow);
     }, aWindow);
   });
 }
 
 function loadTabView(callback, aWindow) {
   showTabView(function () {
     hideTabView(callback, aWindow);
   }, aWindow);
-}
+}
\ No newline at end of file
--- a/browser/config/version.txt
+++ b/browser/config/version.txt
@@ -1,1 +1,1 @@
-25.0a1
+26.0a1
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -14,24 +14,22 @@ this.EXPORTED_SYMBOLS = [ "CmdAddonFlags
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource://gre/modules/osfile.jsm");
 
 Cu.import("resource://gre/modules/devtools/gcli.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 
-var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
+let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+let Telemetry = devtools.require("devtools/shared/telemetry");
 let telemetry = new Telemetry();
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource:///modules/devtools/gDevTools.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "devtools",
-                                  "resource://gre/modules/devtools/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppCacheUtils",
                                   "resource:///modules/devtools/AppCacheUtils.jsm");
 
 /* CmdAddon ---------------------------------------------------------------- */
 
 (function(module) {
   XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                     "resource://gre/modules/AddonManager.jsm");
@@ -794,18 +792,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       return gcli.lookupFormat("cmdStatus", [ commands.length, args.directory ]);
     }
   });
 }(this));
 
 /* CmdConsole -------------------------------------------------------------- */
 
 (function(module) {
-  XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
-                                    "resource:///modules/HUDService.jsm");
+  Object.defineProperty(this, "HUDService", {
+    get: function() {
+      return devtools.require("devtools/webconsole/hudservice");
+    },
+    configurable: true,
+    enumerable: true
+  });
 
   /**
    * 'console' command
    */
   gcli.addCommand({
     name: "console",
     description: gcli.lookup("consoleDesc"),
     manual: gcli.lookup("consoleManual")
--- a/browser/devtools/commandline/test/browser_cmd_calllog.js
+++ b/browser/devtools/commandline/test/browser_cmd_calllog.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the calllog commands works as they should
 
-let HUDService = (Cu.import("resource:///modules/HUDService.jsm", {})).HUDService;
-
 const TEST_URI = "data:text/html;charset=utf-8,gcli-calllog";
 
 let tests = {};
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.runTests(options, tests);
   }).then(finish);
@@ -50,17 +48,17 @@ tests.testCallLogStatus = function(optio
 tests.testCallLogExec = function(options) {
   var deferred = promise.defer();
 
   var onWebConsoleOpen = function(subject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     subject.QueryInterface(Ci.nsISupportsString);
     let hud = HUDService.getHudReferenceById(subject.data);
-    ok(hud.hudId in HUDService.hudReferences, "console open");
+    ok(hud, "console open");
 
     helpers.audit(options, [
       {
         setup: "calllog stop",
         exec: {
           output: /Stopped call logging/,
         }
       },
--- a/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
+++ b/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the calllog commands works as they should
 
-let HUDService = (Cu.import("resource:///modules/HUDService.jsm", {})).HUDService;
-
 const TEST_URI = "data:text/html;charset=utf-8,cmd-calllog-chrome";
 
 let tests = {};
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.runTests(options, tests);
   }).then(finish);
@@ -51,17 +49,17 @@ tests.testCallLogStatus = function(optio
 tests.testCallLogExec = function(options) {
   let deferred = promise.defer();
 
   function onWebConsoleOpen(subject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     subject.QueryInterface(Ci.nsISupportsString);
     let hud = HUDService.getHudReferenceById(subject.data);
-    ok(hud.hudId in HUDService.hudReferences, "console open");
+    ok(hud, "console open");
 
     helpers.audit(options, [
       {
         setup: "calllog chromestop",
         exec: {
           output: /Stopped call logging/,
         }
       },
--- a/browser/devtools/commandline/test/browser_cmd_commands.js
+++ b/browser/devtools/commandline/test/browser_cmd_commands.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test various GCLI commands
 
-let HUDService = (Cu.import("resource:///modules/HUDService.jsm", {})).HUDService;
-
 const TEST_URI = "data:text/html;charset=utf-8,gcli-commands";
 
 let tests = {};
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.runTests(options, tests);
   }).then(finish);
@@ -19,51 +17,56 @@ tests.testConsole = function(options) {
   let deferred = promise.defer();
   let hud = null;
 
   let onWebConsoleOpen = function(subject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     subject.QueryInterface(Ci.nsISupportsString);
     hud = HUDService.getHudReferenceById(subject.data);
-    ok(hud.hudId in HUDService.hudReferences, "console open");
+    ok(hud, "console open");
 
     hud.jsterm.execute("pprint(window)", onExecute);
   }
   Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
 
-  let onExecute = function() {
+  function onExecute () {
     let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
     ok(labels.length > 0, "output for pprint(window)");
 
+    hud.jsterm.once("messages-cleared", onClear);
+
     helpers.audit(options, [
       {
         setup: "console clear",
         exec: {
           output: ""
         },
-        post: function() {
-          let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
-          // Bug 845827 - The GCLI "console clear" command doesn't always work
-          // is(labels.length, 0, "no output in console");
-        }
-      },
+      }
+    ]);
+  }
+
+  function onClear() {
+    let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+    is(labels.length, 0, "no output in console");
+
+    helpers.audit(options, [
       {
         setup: "console close",
         exec: {
           output: ""
         },
         post: function() {
-          ok(!(hud.hudId in HUDService.hudReferences), "console closed");
+          ok(!HUDService.getHudReferenceById(hud.hudId), "console closed");
         }
       }
     ]).then(function() {
       deferred.resolve();
     });
-  };
+  }
 
   helpers.audit(options, [
     {
       setup: "console open",
       exec: { }
     }
   ]);
 
--- a/browser/devtools/debugger/CmdDebugger.jsm
+++ b/browser/devtools/debugger/CmdDebugger.jsm
@@ -501,21 +501,22 @@ gcli.addCommand({
       }
 
       // Send the black box request to each source we are black boxing. As we
       // get responses, accumulate the results in `blackBoxed`.
 
       const blackBoxed = [];
 
       for (let source of toBlackBox) {
+        let { url } = source;
         activeThread.source(source)[cmd.clientMethod](function ({ error }) {
           if (error) {
-            blackBoxed.push(lookup("ErrorDesc") + " " + source.url);
+            blackBoxed.push(lookup("ErrorDesc") + " " + url);
           } else {
-            blackBoxed.push(source.url);
+            blackBoxed.push(url);
           }
 
           if (toBlackBox.length === blackBoxed.length) {
             displayResults();
           }
         });
       }
 
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -24,18 +24,27 @@ Cu.import("resource:///modules/devtools/
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
-  "resource://gre/modules/devtools/NetworkHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+  "resource://gre/modules/devtools/Loader.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  enumerable: true
+});
+
 
 /**
  * Object defining the debugger controller components.
  */
 let DebuggerController = {
   /**
    * Initializes the debugger controller.
    */
@@ -433,16 +442,17 @@ StackFrames.prototype = {
   autoScopeExpand: false,
   currentFrame: null,
   syncedWatchExpressions: null,
   currentWatchExpressions: null,
   currentBreakpointLocation: null,
   currentEvaluation: null,
   currentException: null,
   currentReturnedValue: null,
+  _dontSwitchSources: false,
 
   /**
    * Connect to the current thread client.
    */
   connect: function() {
     dumpn("StackFrames is connecting...");
     this.activeThread.addListener("paused", this._onPaused);
     this.activeThread.addListener("resumed", this._onResumed);
@@ -589,36 +599,50 @@ StackFrames.prototype = {
         return;
       }
     }
 
 
     // Make sure the debugger view panes are visible.
     DebuggerView.showInstrumentsPane();
 
+    this._refillFrames();
+  },
+
+  /**
+   * Fill the StackFrames view with the frames we have in the cache, compressing
+   * frames which have black boxed sources into single frames.
+   */
+  _refillFrames: function() {
     // Make sure all the previous stackframes are removed before re-adding them.
     DebuggerView.StackFrames.empty();
 
     let previousBlackBoxed = null;
     for (let frame of this.activeThread.cachedFrames) {
-      let { depth, where: { url, line }, isBlackBoxed } = frame;
+      let { depth, where: { url, line }, source } = frame;
+
+      let isBlackBoxed = source
+        ? this.activeThread.source(source).isBlackBoxed
+        : false;
       let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
       let frameTitle = StackFrameUtils.getFrameTitle(frame);
 
       if (isBlackBoxed) {
         if (previousBlackBoxed == url) {
           continue;
         }
         previousBlackBoxed = url;
       } else {
         previousBlackBoxed = null;
       }
 
-      DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth, isBlackBoxed);
+      DebuggerView.StackFrames.addFrame(
+        frameTitle, frameLocation, line, depth, isBlackBoxed);
     }
+
     if (this.currentFrame == null) {
       DebuggerView.StackFrames.selectedDepth = 0;
     }
     if (this.activeThread.moreFrames) {
       DebuggerView.StackFrames.dirty = true;
     }
   },
 
@@ -639,20 +663,19 @@ StackFrames.prototype = {
     window.setTimeout(this._afterFramesCleared, FRAME_STEP_CLEAR_DELAY);
   },
 
   /**
    * Handler for the debugger's blackboxchange notification.
    */
   _onBlackBoxChange: function() {
     if (this.activeThread.state == "paused") {
-      // We have to clear out the existing frames and refetch them to get their
-      // updated black boxed status.
-      this.activeThread._clearFrames();
-      this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+      this._dontSwitchSources = true;
+      this.currentFrame = null;
+      this._refillFrames();
     }
   },
 
   /**
    * Called soon after the thread client's framescleared notification.
    */
   _afterFramesCleared: function() {
     // Ignore useless notifications.
@@ -667,32 +690,37 @@ StackFrames.prototype = {
   },
 
   /**
    * Marks the stack frame at the specified depth as selected and updates the
    * properties view with the stack frame's data.
    *
    * @param number aDepth
    *        The depth of the frame in the stack.
+   * @param boolean aDontSwitchSources
+   *        Flag on whether or not we want to switch the selected source.
    */
-  selectFrame: function(aDepth) {
+  selectFrame: function(aDepth, aDontSwitchSources) {
     // Make sure the frame at the specified depth exists first.
     let frame = this.activeThread.cachedFrames[this.currentFrame = aDepth];
     if (!frame) {
       return;
     }
 
     // Check if the frame does not represent the evaluation of debuggee code.
     let { environment, where: { url, line } } = frame;
     if (!environment) {
       return;
     }
 
+    let noSwitch = this._dontSwitchSources;
+    this._dontSwitchSources = false;
+
     // Move the editor's caret to the proper url and line.
-    DebuggerView.updateEditor(url, line);
+    DebuggerView.updateEditor(url, line, { noSwitch: noSwitch });
     // Highlight the breakpoint at the specified url and line if it exists.
     DebuggerView.Sources.highlightBreakpoint(url, line);
     // Don't display the watch expressions textbox inputs in the pane.
     DebuggerView.WatchExpressions.toggleContents(false);
     // Start recording any added variables or properties in any scope.
     DebuggerView.Variables.createHierarchy();
     // Clear existing scopes and create each one dynamically.
     DebuggerView.Variables.empty();
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -188,28 +188,44 @@
          keycode="VK_DOWN"
          modifiers="accel alt"
          command="nextSourceCommand"/>
     <key id="prevSourceKey"
          keycode="VK_UP"
          modifiers="accel alt"
          command="prevSourceCommand"/>
     <key id="resumeKey"
-         keycode="&debuggerUI.stepping.resume;"
+         keycode="&debuggerUI.stepping.resume1;"
+         command="resumeCommand"/>
+    <key id="resumeKey2"
+         keycode="&debuggerUI.stepping.resume2;"
+         modifiers="accel"
          command="resumeCommand"/>
     <key id="stepOverKey"
-         keycode="&debuggerUI.stepping.stepOver;"
+         keycode="&debuggerUI.stepping.stepOver1;"
+         command="stepOverCommand"/>
+    <key id="stepOverKey2"
+         keycode="&debuggerUI.stepping.stepOver2;"
+         modifiers="accel"
          command="stepOverCommand"/>
     <key id="stepInKey"
-         keycode="&debuggerUI.stepping.stepIn;"
+         keycode="&debuggerUI.stepping.stepIn1;"
+         command="stepInCommand"/>
+    <key id="stepInKey2"
+         keycode="&debuggerUI.stepping.stepIn2;"
+         modifiers="accel"
          command="stepInCommand"/>
     <key id="stepOutKey"
-         keycode="&debuggerUI.stepping.stepOut;"
+         keycode="&debuggerUI.stepping.stepOut1;"
          modifiers="shift"
          command="stepOutCommand"/>
+    <key id="stepOutKey2"
+         keycode="&debuggerUI.stepping.stepOut2;"
+         modifiers="accel shift"
+         command="stepOutCommand"/>
     <key id="fileSearchKey"
          key="&debuggerUI.searchFile.key;"
          modifiers="accel"
          command="fileSearchCommand"/>
     <key id="fileSearchKey"
          key="&debuggerUI.searchFile.altkey;"
          modifiers="accel"
          command="fileSearchCommand"/>
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -12,16 +12,18 @@ include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_aaa_run_first_leaktest.js \
 	browser_dbg_blackboxing-01.js \
 	browser_dbg_blackboxing-02.js \
 	browser_dbg_blackboxing-03.js \
 	browser_dbg_blackboxing-04.js \
 	browser_dbg_blackboxing-05.js \
+	browser_dbg_blackboxing-06.js \
+	browser_dbg_blackboxing-07.js \
 	browser_dbg_clean-exit.js \
 	browser_dbg_cmd.js \
 	browser_dbg_cmd_blackbox.js \
 	browser_dbg_cmd_break.js \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs-01.js \
 	browser_dbg_listtabs-02.js \
 	browser_dbg_tabactor-01.js \
--- a/browser/devtools/debugger/test/browser_dbg_blackboxing-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-03.js
@@ -52,27 +52,25 @@ function testBlackBoxStack() {
 }
 
 function testBlackBoxSource() {
   const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
   ok(checkbox, "Should get the checkbox for black boxing the source");
 
   const { activeThread } = gDebugger.DebuggerController;
   activeThread.addOneTimeListener("blackboxchange", function (event, sourceClient) {
-    activeThread.addOneTimeListener("framesadded", function () {
-      ok(sourceClient.isBlackBoxed, "The source should be black boxed now");
+    ok(sourceClient.isBlackBoxed, "The source should be black boxed now");
 
-      const frames = gDebugger.DebuggerView.StackFrames.widget._list;
-      is(frames.querySelectorAll(".dbg-stackframe").length, 3,
-         "Should only get 3 frames");
-      is(frames.querySelectorAll(".dbg-stackframe-black-boxed").length, 1,
-         "And one of them is the combined black boxed frames");
+    const frames = gDebugger.DebuggerView.StackFrames.widget._list;
+    is(frames.querySelectorAll(".dbg-stackframe").length, 3,
+       "Should only get 3 frames");
+    is(frames.querySelectorAll(".dbg-stackframe-black-boxed").length, 1,
+       "And one of them is the combined black boxed frames");
 
-      closeDebuggerAndFinish();
-    });
+    closeDebuggerAndFinish();
   });
 
   checkbox.click();
 }
 
 function getBlackBoxCheckbox(url) {
   return gDebugger.document.querySelector(
     ".side-menu-widget-item[tooltiptext=\""
--- a/browser/devtools/debugger/test/browser_dbg_blackboxing-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-05.js
@@ -36,17 +36,17 @@ function testSourceEditorShown() {
   is(deck.selectedIndex, "0",
      "The first item in the deck should be selected (the source editor)");
   blackBoxSource();
 }
 
 function blackBoxSource() {
   const { activeThread } = gDebugger.DebuggerController;
   activeThread.addOneTimeListener("blackboxchange", testBlackBoxMessageShown);
-  getBlackBoxCheckbox().click();
+  getAnyBlackBoxCheckbox().click();
 }
 
 function testBlackBoxMessageShown() {
   const deck = gDebugger.document.getElementById("editor-deck");
   is(deck.selectedIndex, "1",
      "The second item in the deck should be selected (the black box message)");
   clickStopBlackBoxingButton();
 }
@@ -60,17 +60,17 @@ function clickStopBlackBoxingButton() {
 
 function testSourceEditorShownAgain() {
   const deck = gDebugger.document.getElementById("editor-deck");
   is(deck.selectedIndex, "0",
      "The first item in the deck should be selected again (the source editor)");
   closeDebuggerAndFinish();
 }
 
-function getBlackBoxCheckbox() {
+function getAnyBlackBoxCheckbox() {
   return gDebugger.document.querySelector(
     ".side-menu-widget-item .side-menu-widget-item-checkbox");
 }
 
 function once(target, event, callback) {
   target.addEventListener(event, function _listener(...args) {
     target.removeEventListener(event, _listener, false);
     callback.apply(null, args);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-06.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that clicking the black box checkbox doesn't select that source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_blackboxing.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test()
+{
+  let scriptShown = false;
+  let framesAdded = false;
+  let resumed = false;
+  let testStarted = false;
+
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    resumed = true;
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPane = aPane;
+    gDebugger = gPane.panelWin;
+
+    once(gDebugger, "Debugger:SourceShown", testBlackBox);
+  });
+}
+
+function testBlackBox() {
+  const sources = gDebugger.DebuggerView.Sources;
+
+  const selectedUrl = sources.selectedItem.attachment.source.url;
+  const checkbox = getDifferentBlackBoxCheckbox(selectedUrl);
+  ok(checkbox, "We should be able to grab a checkbox");
+
+  const { activeThread } = gDebugger.DebuggerController;
+  activeThread.addOneTimeListener("blackboxchange", function () {
+    is(selectedUrl,
+       sources.selectedItem.attachment.source.url,
+       "The same source should be selected");
+    closeDebuggerAndFinish();
+  });
+
+  checkbox.click();
+}
+
+function getDifferentBlackBoxCheckbox(url) {
+  return gDebugger.document.querySelector(
+    ".side-menu-widget-item:not([tooltiptext=\""
+      + url + "\"]) .side-menu-widget-item-checkbox");
+}
+
+function once(target, event, callback) {
+  target.addEventListener(event, function _listener(...args) {
+    target.removeEventListener(event, _listener, false);
+    callback.apply(null, args);
+  }, false);
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebuggee = null;
+  gDebugger = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-07.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that clicking the black box checkbox when paused doesn't re-select the
+ * currently paused frame's source.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_blackboxing.html";
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+
+function test()
+{
+  let scriptShown = false;
+  let framesAdded = false;
+  let resumed = false;
+  let testStarted = false;
+
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    resumed = true;
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPane = aPane;
+    gDebugger = gPane.panelWin;
+
+    once(gDebugger, "Debugger:SourceShown", runTest);
+  });
+}
+
+function runTest() {
+  const { activeThread } = gDebugger.DebuggerController;
+  activeThread.addOneTimeListener("paused", function () {
+    const sources = gDebugger.DebuggerView.Sources;
+    const selectedUrl = sources.selectedItem.attachment.source.url;
+
+    once(gDebugger, "Debugger:SourceShown", function () {
+      const newSelectedUrl = sources.selectedItem.attachment.source.url;
+      isnot(selectedUrl, newSelectedUrl,
+            "Should not have the same url selected");
+
+      activeThread.addOneTimeListener("blackboxchange", function () {
+        isnot(sources.selectedItem.attachment.source.url,
+              selectedUrl,
+              "The selected source did not change");
+        closeDebuggerAndFinish();
+      });
+
+      getBlackBoxCheckbox(newSelectedUrl).click();
+    });
+
+    getDifferentSource(selectedUrl).click();
+  });
+
+  gDebuggee.runTest();
+}
+
+function getDifferentSource(url) {
+  return gDebugger.document.querySelector(
+    ".side-menu-widget-item:not([tooltiptext=\""
+      + url + "\"])");
+}
+
+function getBlackBoxCheckbox(url) {
+  return gDebugger.document.querySelector(
+    ".side-menu-widget-item[tooltiptext=\""
+      + url + "\"] .side-menu-widget-item-checkbox");
+}
+
+function once(target, event, callback) {
+  target.addEventListener(event, function _listener(...args) {
+    target.removeEventListener(event, _listener, false);
+    callback.apply(null, args);
+  }, false);
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebuggee = null;
+  gDebugger = null;
+});
--- a/browser/devtools/debugger/test/browser_dbg_cmd_blackbox.js
+++ b/browser/devtools/debugger/test/browser_dbg_cmd_blackbox.js
@@ -15,31 +15,37 @@ let gcli = Cu.import("resource://gre/mod
 let gTarget;
 let gPanel;
 let gOptions;
 let gDebugger;
 let gClient;
 let gThreadClient;
 let gTab;
 
-function cmd(typed, expectedNumEvents=1) {
+function cmd(typed, expectedNumEvents=1, output=null) {
   const deferred = promise.defer();
 
   let timesFired = 0;
   gThreadClient.addListener("blackboxchange", function _onBlackBoxChange() {
     if (++timesFired === expectedNumEvents) {
       gThreadClient.removeListener("blackboxchange", _onBlackBoxChange);
       deferred.resolve();
     }
   });
 
-  helpers.audit(gOptions, [{
+  let audit = {
     setup: typed,
     exec: {}
-  }]);
+  };
+
+  if (output) {
+    audit.output = output;
+  }
+
+  helpers.audit(gOptions, [audit]);
 
   return deferred.promise;
 }
 
 function test() {
   helpers.addTabWithToolbar(TEST_URL, function(options) {
     gOptions = options;
     gTarget = options.target;
@@ -88,17 +94,18 @@ function testUnBlackBoxSource() {
     .then(function () {
       const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
       ok(checkbox.checked,
          "Should be able to stop black boxing a specific source");
     });
 }
 
 function testBlackBoxGlob() {
-  return cmd("dbg blackbox --glob *blackboxing_t*.js", 2)
+  return cmd("dbg blackbox --glob *blackboxing_t*.js", 2,
+             [/blackboxing_three\.js/g, /blackboxing_two\.js/g])
     .then(function () {
       ok(getBlackBoxCheckbox(BLACKBOXME_URL).checked,
          "blackboxme should not be black boxed because it doesn't match the glob");
       ok(getBlackBoxCheckbox(BLACKBOXONE_URL).checked,
          "blackbox_one should not be black boxed because it doesn't match the glob");
 
       ok(!getBlackBoxCheckbox(BLACKBOXTWO_URL).checked,
          "blackbox_two should be black boxed because it matches the glob");
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -14,16 +14,17 @@ Cu.import("resource:///modules/devtools/
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 
 var ProfilerController = devtools.require("devtools/profiler/controller");
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
+
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
 this.DevTools = function DevTools() {
   this._tools = new Map();     // Map<toolId, tool>
   this._toolboxes = new Map(); // Map<target, toolbox>
 
@@ -340,25 +341,34 @@ let gDevToolsBrowser = {
    * - if the toolbox is open, and the targetted tool is selected,
    *   and the host is NOT a window, we close the toolbox
    * - if the toolbox is open, and the targetted tool is selected,
    *   and the host is a window, we raise the toolbox window
    */
   selectToolCommand: function(gBrowser, toolId) {
     let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
+    let tools = gDevTools.getToolDefinitionMap();
+    let toolDefinition = tools.get(toolId);
 
     if (toolbox && toolbox.currentToolId == toolId) {
-      if (toolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+      toolbox.fireCustomKey(toolId);
+
+      if (toolDefinition.preventClosingOnKey || toolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
         toolbox.raise();
       } else {
         toolbox.destroy();
       }
     } else {
-      gDevTools.showToolbox(target, toolId);
+      gDevTools.showToolbox(target, toolId).then(() => {
+        let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+        let toolbox = gDevTools.getToolbox(target);
+
+        toolbox.fireCustomKey(toolId);
+      });
     }
   },
 
   /**
    * Open a tab to allow connects to a remote browser
    */
   openConnectScreen: function(gBrowser) {
     gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/connect.xhtml");
@@ -369,16 +379,21 @@ let gDevToolsBrowser = {
    *
    * @param {XULDocument} doc
    *        The document to which menuitems and handlers are to be added
    */
   registerBrowserWindow: function DT_registerBrowserWindow(win) {
     gDevToolsBrowser._trackedBrowserWindows.add(win);
     gDevToolsBrowser._addAllToolsToMenu(win.document);
 
+    if (this._isFirebugInstalled()) {
+      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+      broadcaster.removeAttribute("key");
+    }
+
     let tabContainer = win.document.getElementById("tabbrowser-tabs")
     tabContainer.addEventListener("TabSelect",
                                   gDevToolsBrowser._updateMenuCheckbox, false);
   },
 
   /**
    * Add a <key> to <keyset id="devtoolsKeyset">.
    * Appending a <key> element is not always enough. The <keyset> needs
@@ -387,25 +402,37 @@ let gDevToolsBrowser = {
    *
    * @param {XULDocument} doc
    *        The document to which keys are to be added
    * @param {XULElement} or {DocumentFragment} keys
    *        Keys to add
    */
   attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) {
     let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
+
     if (!devtoolsKeyset) {
       devtoolsKeyset = doc.createElement("keyset");
       devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
     }
     devtoolsKeyset.appendChild(keys);
     let mainKeyset = doc.getElementById("mainKeyset");
     mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
   },
 
+
+  /**
+   * Detect the presence of a Firebug.
+   *
+   * @return promise
+   */
+  _isFirebugInstalled: function DT_isFirebugInstalled() {
+    let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons");
+    return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1;
+  },
+
   /**
    * Add the menuitem for a tool to all open browser windows.
    *
    * @param {object} toolDefinition
    *        properties of the tool to add
    */
   _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
     // No menu item or global shortcut is required for options panel.
@@ -720,16 +747,17 @@ let gDevToolsBrowser = {
   /**
    * All browser windows have been closed, tidy up remaining objects.
    */
   destroy: function() {
     gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler);
     Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
   },
 }
+
 this.gDevToolsBrowser = gDevToolsBrowser;
 
 gDevTools.on("tool-registered", function(ev, toolId) {
   let toolDefinition = gDevTools._tools.get(toolId);
   gDevToolsBrowser._addToolToWindows(toolDefinition);
 });
 
 gDevTools.on("tool-unregistered", function(ev, toolId) {
--- a/browser/devtools/framework/test/Makefile.in
+++ b/browser/devtools/framework/test/Makefile.in
@@ -25,11 +25,12 @@ MOCHITEST_BROWSER_FILES = \
 		browser_toolbox_tabsswitch_shortcuts.js \
 		browser_toolbox_window_title_changes.js \
 		browser_toolbox_options.js \
 		browser_toolbox_options_disablejs.js \
 		browser_toolbox_options_disablejs.html \
 		browser_toolbox_options_disablejs_iframe.html \
 		browser_toolbox_highlight.js \
 		browser_toolbox_raise.js \
+		browser_keybindings.js \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_keybindings.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the keybindings for opening and closing the inspector work as expected
+// Can probably make this a shared test that tests all of the tools global keybindings
+
+function test()
+{
+  waitForExplicitFinish();
+
+  let doc;
+  let node;
+  let inspector;
+  let keysetMap = { };
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupKeyBindingsTest, content);
+  }, true);
+
+  content.location = "data:text/html,<html><head><title>Test for the " +
+                     "highlighter keybindings</title></head><body>" +
+                     "<h1>Keybindings!</h1></body></html>";
+
+  function buildDevtoolsKeysetMap(keyset) {
+    [].forEach.call(keyset.querySelectorAll("key"), function(key) {
+
+      if (!key.getAttribute("key")) {
+        return;
+      }
+
+      let modifiers = key.getAttribute("modifiers");
+
+      keysetMap[key.id.split("_")[1]] = {
+        key: key.getAttribute("key"),
+        modifiers: modifiers,
+        modifierOpt: {
+          shiftKey: modifiers.match("shift"),
+          ctrlKey: modifiers.match("ctrl"),
+          altKey: modifiers.match("alt"),
+          metaKey: modifiers.match("meta"),
+          accelKey: modifiers.match("accel")
+        },
+        synthesizeKey: function() {
+          EventUtils.synthesizeKey(this.key, this.modifierOpt);
+        }
+      }
+    });
+  }
+
+  function setupKeyBindingsTest()
+  {
+    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+      buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset"));
+    }
+
+    gDevTools.once("toolbox-ready", (e, toolbox) => {
+      inspectorShouldBeOpenAndHighlighting(toolbox.getCurrentPanel(), toolbox)
+    });
+
+    keysetMap.inspector.synthesizeKey();
+  }
+
+  function inspectorShouldBeOpenAndHighlighting(aInspector, aToolbox)
+  {
+    is (aToolbox.currentToolId, "inspector", "Correct tool has been loaded");
+    is (aInspector.highlighter.locked, true, "Highlighter should be locked");
+
+    aInspector.highlighter.once("unlocked", () => {
+      is (aInspector.highlighter.locked, false, "Highlighter should be unlocked");
+      keysetMap.inspector.synthesizeKey();
+      is (aInspector.highlighter.locked, true, "Highlighter should be locked");
+      keysetMap.inspector.synthesizeKey();
+      is (aInspector.highlighter.locked, false, "Highlighter should be unlocked");
+      keysetMap.inspector.synthesizeKey();
+      is (aInspector.highlighter.locked, true, "Highlighter should be locked");
+
+      aToolbox.once("webconsole-ready", (e, panel) => {
+        webconsoleShouldBeSelected(aToolbox, panel);
+      });
+      keysetMap.webconsole.synthesizeKey();
+    });
+  }
+
+  function webconsoleShouldBeSelected(aToolbox, panel)
+  {
+      is (aToolbox.currentToolId, "webconsole");
+
+      aToolbox.once("jsdebugger-ready", (e, panel) => {
+        jsdebuggerShouldBeSelected(aToolbox, panel);
+      });
+      keysetMap.jsdebugger.synthesizeKey();
+  }
+
+  function jsdebuggerShouldBeSelected(aToolbox, panel)
+  {
+      is (aToolbox.currentToolId, "jsdebugger");
+
+      aToolbox.once("netmonitor-ready", (e, panel) => {
+        netmonitorShouldBeSelected(aToolbox, panel);
+      });
+
+      keysetMap.netmonitor.synthesizeKey();
+  }
+
+  function netmonitorShouldBeSelected(aToolbox, panel)
+  {
+      is (aToolbox.currentToolId, "netmonitor");
+      finishUp();
+  }
+
+  function finishUp() {
+    doc = node = inspector = keysetMap = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -259,23 +259,40 @@ Toolbox.prototype = {
           key.setAttribute("keycode", toolDefinition.key);
         } else {
           key.setAttribute("key", toolDefinition.key);
         }
 
         key.setAttribute("modifiers", toolDefinition.modifiers);
         key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
         key.addEventListener("command", function(toolId) {
-          this.selectTool(toolId);
+          this.selectTool(toolId).then(() => {
+              this.fireCustomKey(toolId);
+          });
         }.bind(this, id), true);
         doc.getElementById("toolbox-keyset").appendChild(key);
       }
     }
   },
 
+
+  /**
+   * Handle any custom key events.  Returns true if there was a custom key binding run
+   * @param {string} toolId
+   *        Which tool to run the command on (skip if not current)
+   */
+  fireCustomKey: function TBOX_fireCustomKey(toolId) {
+    let tools = gDevTools.getToolDefinitionMap();
+    let activeToolDefinition = tools.get(toolId);
+
+    if (activeToolDefinition.onkey && this.currentToolId === toolId) {
+        activeToolDefinition.onkey(this.getCurrentPanel());
+    }
+  },
+
   /**
    * Build the buttons for changing hosts. Called every time
    * the host changes.
    */
   _buildDockButtons: function TBOX_createDockButtons() {
     let dockBox = this.doc.getElementById("toolbox-dock-buttons");
 
     while (dockBox.firstChild) {
--- a/browser/devtools/inspector/highlighter.js
+++ b/browser/devtools/inspector/highlighter.js
@@ -124,18 +124,16 @@ Highlighter.prototype = {
     // Insert the highlighter right after the browser
     stack.insertBefore(this.highlighterContainer, stack.childNodes[1]);
 
     this.buildInfobar(controlsBox);
 
     this.transitionDisabler = null;
     this.pageEventsMuter = null;
 
-    this.unlockAndFocus();
-
     this.selection.on("new-node", this.highlight);
     this.selection.on("new-node", this.updateInfobar);
     this.selection.on("pseudoclass", this.updateInfobar);
     this.selection.on("attribute-changed", this.updateInfobar);
 
     this.onToolSelected = function(event, id) {
       if (id != "inspector") {
         this.chromeWin.clearTimeout(this.pageEventsMuter);
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -132,20 +132,16 @@ InspectorPanel.prototype = {
     this.isReady = false;
 
     this.once("markuploaded", function() {
       this.isReady = true;
 
       // All the components are initialized. Let's select a node.
       this._selection.setNodeFront(defaultSelection);
 
-      if (this.highlighter) {
-        this.highlighter.unlock();
-      }
-
       this.markup.expandNode(this.selection.nodeFront);
 
       this.emit("ready");
       deferred.resolve(this);
     }.bind(this));
 
     this.setupSearchBox();
     this.setupSidebar();
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,21 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cu} = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("sdk/core/promise");
 
-loader.lazyGetter(this, "AutocompletePopup", () => {
-  return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
-});
+loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
 /**
  * Converts any input box on a page to a CSS selector search and suggestion box.
  *
  * @constructor
--- a/browser/devtools/inspector/test/browser_inspector_bug_665880.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_665880.js
@@ -24,16 +24,17 @@ function test()
     objectNode = doc.querySelector("object");
     ok(objectNode, "we have the object node");
     openInspector(runObjectInspectionTest);
   }
 
   function runObjectInspectionTest(inspector)
   {
     inspector.highlighter.once("locked", performTestComparison);
+    inspector.highlighter.unlock();
     inspector.selection.setNode(objectNode, "");
   }
 
   function performTestComparison()
   {
     is(getActiveInspector().selection.node, objectNode, "selection matches node");
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     executeSoon(function() {
--- a/browser/devtools/inspector/test/browser_inspector_bug_674871.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_674871.js
@@ -45,16 +45,17 @@ function test()
     iframeBodyNode = iframeNode.contentDocument.querySelector("body");
     ok(iframeNode, "we have the iframe node");
     ok(iframeBodyNode, "we have the body node");
     openInspector(runTests);
   }
 
   function runTests(inspector)
   {
+    inspector.highlighter.unlock();
     executeSoon(function() {
       inspector.highlighter.once("highlighting", isTheIframeSelected);
       moveMouseOver(iframeNode, 1, 1);
     });
   }
 
   function isTheIframeSelected()
   {
--- a/browser/devtools/inspector/test/browser_inspector_iframeTest.js
+++ b/browser/devtools/inspector/test/browser_inspector_iframeTest.js
@@ -46,16 +46,17 @@ function createDocument()
 function moveMouseOver(aElement)
 {
   EventUtils.synthesizeMouse(aElement, 2, 2, {type: "mousemove"},
     aElement.ownerDocument.defaultView);
 }
 
 function runIframeTests()
 {
+  getActiveInspector().highlighter.unlock();
   getActiveInspector().selection.once("new-node", performTestComparisons1);
   moveMouseOver(div1)
 }
 
 function performTestComparisons1()
 {
   let i = getActiveInspector();
   is(i.selection.node, div1, "selection matches div1 node");
--- a/browser/devtools/inspector/test/browser_inspector_scrolling.js
+++ b/browser/devtools/inspector/test/browser_inspector_scrolling.js
@@ -30,16 +30,17 @@ function createDocument()
 }
 
 function inspectNode(aInspector)
 {
   inspector = aInspector;
 
   inspector.highlighter.once("locked", performScrollingTest);
   executeSoon(function() {
+    inspector.highlighter.unlock();
     inspector.selection.setNode(div, "");
   });
 }
 
 function performScrollingTest()
 {
   executeSoon(function() {
     EventUtils.synthesizeWheel(div, 10, 10,
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -7,18 +7,17 @@ browser.jar:
     content/browser/devtools/widgets/VariablesView.xul                 (shared/widgets/VariablesView.xul)
     content/browser/devtools/markup-view.xhtml                         (markupview/markup-view.xhtml)
     content/browser/devtools/markup-view.css                           (markupview/markup-view.css)
     content/browser/devtools/netmonitor.xul                            (netmonitor/netmonitor.xul)
     content/browser/devtools/netmonitor.css                            (netmonitor/netmonitor.css)
     content/browser/devtools/netmonitor-controller.js                  (netmonitor/netmonitor-controller.js)
     content/browser/devtools/netmonitor-view.js                        (netmonitor/netmonitor-view.js)
     content/browser/devtools/NetworkPanel.xhtml                        (webconsole/NetworkPanel.xhtml)
-    content/browser/devtools/webconsole.js                             (webconsole/webconsole.js)
-*   content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
+    content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
 *   content/browser/devtools/scratchpad.xul                            (scratchpad/scratchpad.xul)
     content/browser/devtools/scratchpad.js                             (scratchpad/scratchpad.js)
     content/browser/devtools/splitview.css                             (shared/splitview.css)
     content/browser/devtools/theme-switching.js                        (shared/theme-switching.js)
     content/browser/devtools/styleeditor.xul                           (styleeditor/styleeditor.xul)
     content/browser/devtools/styleeditor.css                           (styleeditor/styleeditor.css)
     content/browser/devtools/computedview.xhtml                        (styleinspector/computedview.xhtml)
     content/browser/devtools/cssruleview.xhtml                         (styleinspector/cssruleview.xhtml)
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -14,20 +14,22 @@ Object.defineProperty(exports, "Toolbox"
   get: () => require("devtools/framework/toolbox").Toolbox
 });
 Object.defineProperty(exports, "TargetFactory", {
   get: () => require("devtools/framework/target").TargetFactory
 });
 
 loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
 
+let events = require("sdk/system/events");
+
 // Panels
 loader.lazyGetter(this, "OptionsPanel", function() require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", function() require("devtools/inspector/inspector-panel").InspectorPanel);
-loader.lazyImporter(this, "WebConsolePanel", "resource:///modules/WebConsolePanel.jsm");
+loader.lazyGetter(this, "WebConsolePanel", function() require("devtools/webconsole/panel").WebConsolePanel);
 loader.lazyImporter(this, "DebuggerPanel", "resource:///modules/devtools/DebuggerPanel.jsm");
 loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
 loader.lazyGetter(this, "ProfilerPanel", function() require("devtools/profiler/panel"));
 loader.lazyImporter(this, "NetMonitorPanel", "resource:///modules/devtools/NetMonitorPanel.jsm");
 
 // Strings
 const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
@@ -90,16 +92,23 @@ Tools.inspector = {
   key: l10n("inspector.commandkey", inspectorStrings),
   ordinal: 2,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   icon: "chrome://browser/skin/devtools/tool-inspector.png",
   url: "chrome://browser/content/devtools/inspector/inspector.xul",
   label: l10n("inspector.label", inspectorStrings),
   tooltip: l10n("inspector.tooltip", inspectorStrings),
 
+  preventClosingOnKey: true,
+  onkey: function(panel) {
+    if (panel.highlighter) {
+      panel.highlighter.toggleLockState();
+    }
+  },
+
   isTargetSupported: function(target) {
     return !target.isRemote;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new InspectorPanel(iframeWindow, toolbox);
     return panel.open();
   }
@@ -216,16 +225,18 @@ var unloadObserver = {
       for (let definition of gDevTools.getToolDefinitionArray()) {
         gDevTools.unregisterTool(definition.id);
       }
     }
   }
 };
 Services.obs.addObserver(unloadObserver, "sdk:loader:destroy", false);
 
+events.emit("devtools-loaded", {});
+
 /**
  * Lookup l10n string from a string bundle.
  *
  * @param {string} name
  *        The key to lookup.
  * @param {StringBundle} bundle
  *        The key to lookup.
  * @returns A localized version of the given key.
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -16,19 +16,17 @@ let {UndoStack} = require("devtools/shar
 let EventEmitter = require("devtools/shared/event-emitter");
 let {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 let promise = require("sdk/core/promise");
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
-  return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
-});
+loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 
 /**
  * Vocabulary for the purposes of this file:
  *
  * MarkupContainer - the structure that holds an editor and its
  *  immediate children in the markup panel.
  * Node - A content node.
  * object.elt - A UI element in the markup panel.
@@ -156,16 +154,24 @@ MarkupView.prototype = {
 
     // Ignore keystrokes that originated in editors.
     if (aEvent.target.tagName.toLowerCase() === "input" ||
         aEvent.target.tagName.toLowerCase() === "textarea") {
       return;
     }
 
     switch(aEvent.keyCode) {
+      case Ci.nsIDOMKeyEvent.DOM_VK_H:
+        let node = this._selectedContainer.node;
+        if (node.hidden) {
+          this.walker.unhideNode(node).then(() => this.nodeChanged(node));
+        } else {
+          this.walker.hideNode(node).then(() => this.nodeChanged(node));
+        }
+        break;
       case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
       case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
         this.deleteNode(this._selectedContainer.node);
         break;
       case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
         let rootContainer = this._containers.get(this._rootNode);
         this.navigate(rootContainer.children.firstChild.container);
         break;
@@ -518,17 +524,17 @@ MarkupView.prototype = {
     }
   },
 
   /**
    * Called when the markup panel initiates a change on a node.
    */
   nodeChanged: function MT_nodeChanged(aNode)
   {
-    if (aNode === this._inspector.selection) {
+    if (aNode === this._inspector.selection.nodeFront) {
       this._inspector.change("markupview");
     }
   },
 
   /**
    * Check if the current selection is a descendent of the container.
    * if so, make sure it's among the visible set for the container,
    * and set the dirty flag if needed.
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -14,18 +14,26 @@ Cu.import("resource:///modules/source-ed
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
-  "resource://gre/modules/devtools/NetworkHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+  "resource://gre/modules/devtools/Loader.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  enumerable: true
+});
 
 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
   "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
 
 const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
 const LISTENERS = [ "NetworkActivity" ];
 const NET_PREFS = { "NetworkMonitor.saveRequestAndResponseBodies": true };
 
--- a/browser/devtools/profiler/test/head.js
+++ b/browser/devtools/profiler/test/head.js
@@ -14,19 +14,16 @@ Cu.import("resource:///modules/devtools/
 let gDevTools = temp.gDevTools;
 
 Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
 let TargetFactory = temp.devtools.TargetFactory;
 
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp);
 let DebuggerServer = temp.DebuggerServer;
 
-Cu.import("resource:///modules/HUDService.jsm", temp);
-let HUDService = temp.HUDService;
-
 // Import the GCLI test helper
 let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
 
 registerCleanupFunction(function () {
   helpers = null;
   Services.prefs.clearUserPref(PROFILER_ENABLED);
   Services.prefs.clearUserPref(REMOTE_ENABLED);
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js
@@ -1,16 +1,12 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let tempScope = {};
-Cu.import("resource:///modules/HUDService.jsm", tempScope);
-let HUDService = tempScope.HUDService;
-
 function test()
 {
   waitForExplicitFinish();
 
   // To test for this bug we open a Scratchpad window, save its
   // reference and then open another one. This way the first window
   // loses its focus.
   //
@@ -38,17 +34,17 @@ function test()
           let hud = HUDService.getHudReferenceById(subj.data);
           hud.jsterm.clearOutput(true);
           executeSoon(testFocus.bind(null, sw, hud));
         }
 
         Services.obs.
           addObserver(onWebConsoleOpen, "web-console-created", false);
 
-        HUDService.consoleUI.toggleHUD();
+        HUDService.toggleWebConsole();
       });
     });
   }, true);
 
   content.location = "data:text/html;charset=utf8,<p>test window focus for Scratchpad.";
 }
 
 function testFocus(sw, hud) {
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -20,19 +20,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/devtools/Console.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gcli",
                                   "resource://gre/modules/devtools/gcli.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CmdCommands",
                                   "resource:///modules/devtools/BuiltinCommands.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ConsoleServiceListener",
-                                  "resource://gre/modules/devtools/WebConsoleUtils.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource://gre/modules/devtools/Loader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "require",
                                   "resource://gre/modules/devtools/Require.jsm");
@@ -50,16 +47,24 @@ XPCOMUtils.defineLazyGetter(this, "prefB
 XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () {
   return Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
 });
 
 let Telemetry = devtools.require("devtools/shared/telemetry");
 
 const converters = require("gcli/converters");
 
+Object.defineProperty(this, "ConsoleServiceListener", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/utils").ConsoleServiceListener;
+  },
+  configurable: true,
+  enumerable: true
+});
+
 /**
  * A collection of utilities to help working with commands
  */
 let CommandUtils = {
   /**
    * Read a toolbarSpec from preferences
    * @param aPref The name of the preference to read
    */
@@ -395,16 +400,20 @@ DeveloperToolbar.prototype._onload = fun
 
   let tabbrowser = this._chromeWindow.getBrowser();
   tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
   tabbrowser.tabContainer.addEventListener("TabClose", this, false);
   tabbrowser.addEventListener("load", this, true);
   tabbrowser.addEventListener("beforeunload", this, true);
 
   this._initErrorsCount(tabbrowser.selectedTab);
+  this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this);
+  this._devtoolsLoaded = this._devtoolsLoaded.bind(this);
+  Services.obs.addObserver(this._devtoolsUnloaded, "devtools-unloaded", false);
+  Services.obs.addObserver(this._devtoolsLoaded, "devtools-loaded", false);
 
   this._element.hidden = false;
 
   if (aFocus) {
     this._input.focus();
   }
 
   this._notify(NOTIFICATIONS.SHOW);
@@ -423,16 +432,36 @@ DeveloperToolbar.prototype._onload = fun
 
   if (!DeveloperToolbar.introShownThisSession) {
     this.display.maybeShowIntro();
     DeveloperToolbar.introShownThisSession = true;
   }
 };
 
 /**
+ * The devtools-unloaded event handler.
+ * @private
+ */
+DeveloperToolbar.prototype._devtoolsUnloaded = function DT__devtoolsUnloaded()
+{
+  let tabbrowser = this._chromeWindow.getBrowser();
+  Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
+};
+
+/**
+ * The devtools-loaded event handler.
+ * @private
+ */
+DeveloperToolbar.prototype._devtoolsLoaded = function DT__devtoolsLoaded()
+{
+  let tabbrowser = this._chromeWindow.getBrowser();
+  this._initErrorsCount(tabbrowser.selectedTab);
+};
+
+/**
  * Initialize the listeners needed for tracking the number of errors for a given
  * tab.
  *
  * @private
  * @param nsIDOMNode aTab the xul:tab for which you want to track the number of
  * errors.
  */
 DeveloperToolbar.prototype._initErrorsCount = function DT__initErrorsCount(aTab)
@@ -518,16 +547,18 @@ DeveloperToolbar.prototype.destroy = fun
   }
 
   let tabbrowser = this._chromeWindow.getBrowser();
   tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
   tabbrowser.tabContainer.removeEventListener("TabClose", this, false);
   tabbrowser.removeEventListener("load", this, true);
   tabbrowser.removeEventListener("beforeunload", this, true);
 
+  Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded");
+  Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded");
   Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
 
   this.display.focusManager.removeMonitoredElement(this.outputPanel._frame);
   this.display.focusManager.removeMonitoredElement(this._element);
 
   this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel);
   this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
   this.display.onOutput.remove(this.outputPanel._outputChanged, this.outputPanel);
rename from browser/devtools/shared/AutocompletePopup.jsm
rename to browser/devtools/shared/autocomplete-popup.js
--- a/browser/devtools/shared/AutocompletePopup.jsm
+++ b/browser/devtools/shared/autocomplete-popup.js
@@ -1,27 +1,20 @@
 /* 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/. */
 
-const Cu = Components.utils;
-const Ci = Components.interfaces;
+"use strict";
 
-// The XUL and XHTML namespace.
+const {Cc, Ci, Cu} = require("chrome");
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "gDevTools", function() {
-  return Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools;
-});
-
-this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
+loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 
 /**
  * Autocomplete popup UI implementation.
  *
  * @constructor
  * @param nsIDOMDocument aDocument
  *        The document you want the popup attached to.
  * @param Object aOptions
@@ -33,17 +26,16 @@ this.EXPORTED_SYMBOLS = ["AutocompletePo
  *        - autoSelect {Boolean} Boolean to allow the first entry of the popup
  *                     panel to be automatically selected when the popup shows.
  *        - fixedWidth {Boolean} Boolean to control dynamic width of the popup.
  *        - direction {String} The direction of the text in the panel. rtl or ltr
  *        - onSelect {String} The select event handler for the richlistbox
  *        - onClick {String} The click event handler for the richlistbox.
  *        - onKeypress {String} The keypress event handler for the richlistitems.
  */
-this.AutocompletePopup =
 function AutocompletePopup(aDocument, aOptions = {})
 {
   this._document = aDocument;
 
   this.fixedWidth = aOptions.fixedWidth || false;
   this.autoSelect = aOptions.autoSelect || false;
   this.position = aOptions.position || "after_start";
   this.direction = aOptions.direction || "ltr";
@@ -112,16 +104,17 @@ function AutocompletePopup(aDocument, aO
   if (this.onClick) {
     this._list.addEventListener("click", this.onClick, false);
   }
 
   if (this.onKeypress) {
     this._list.addEventListener("keypress", this.onKeypress, false);
   }
 }
+exports.AutocompletePopup = AutocompletePopup;
 
 AutocompletePopup.prototype = {
   _document: null,
   _panel: null,
   _list: null,
 
   // Event handlers.
   onSelect: null,
@@ -263,32 +256,38 @@ AutocompletePopup.prototype = {
 
   /**
    * Update the panel size to fit the content.
    *
    * @private
    */
   _updateSize: function AP__updateSize()
   {
-    // We need the dispatch to allow the content to reflow. Attempting to
-    // update the richlistbox size too early does not work.
-    Services.tm.currentThread.dispatch({ run: () => {
-      if (!this._panel) {
-        return;
-      }
-      this._list.width = this._panel.clientWidth + this._scrollbarWidth;
-      // Height change is required, otherwise the panel is drawn at an offset
-      // the first time.
-      this._list.height = this._list.clientHeight;
-      // This brings the panel back at right position.
-      this._list.top = 0;
-      // Changing panel height might make the selected item out of view, so
-      // bring it back to view.
-      this._list.ensureIndexIsVisible(this._list.selectedIndex);
-    }}, 0);
+    if (!this._panel) {
+      return;
+    }
+    // Flush the layout so that we get the latest height.
+    this._panel.boxObject.height;
+    let height = {};
+    this._list.scrollBoxObject.getScrolledSize({}, height);
+    // Change the width of the popup only if the scrollbar is visible.
+    if (height.value > this._panel.clientHeight) {
+       this._list.width = this._panel.clientWidth + this._scrollbarWidth;
+    }
+    // Height change is required, otherwise the panel is drawn at an offset
+    // the first time.
+    this._list.height = this._list.clientHeight;
+    // This brings the panel back at right position.
+    this._list.top = 0;
+    // Move the panel to -1,-1 to realign the popup with its anchor node when
+    // decreasing the panel height.
+    this._panel.moveTo(-1, -1);
+    // Changing panel height might make the selected item out of view, so
+    // bring it back to view.
+    this._list.ensureIndexIsVisible(this._list.selectedIndex);
   },
 
   /**
    * Clear all the items from the autocomplete list.
    */
   clearItems: function AP_clearItems()
   {
     // Reset the selectedIndex to -1 before clearing the list
--- a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
+++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -2,18 +2,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the developer toolbar errors count works properly.
 
 function test() {
   const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/" +
                    "browser_toolbar_webconsole_errors_count.html";
 
-  let HUDService = Cu.import("resource:///modules/HUDService.jsm",
-                             {}).HUDService;
   let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
                              {}).gDevTools;
 
   let webconsole = document.getElementById("developer-toolbar-toolbox-button");
   let tab1, tab2;
 
   Services.prefs.setBoolPref("javascript.options.strict", true);
 
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -9,18 +9,26 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
-  "resource://gre/modules/devtools/NetworkHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+  "resource://gre/modules/devtools/Loader.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  enumerable: true
+});
 
 this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
 
 /**
  * A simple side menu, with the ability of grouping menu items.
  * This widget should be used in tandem with the WidgetMethods in ViewHelpers.jsm
  *
  * @param nsIDOMNode aNode
@@ -624,17 +632,22 @@ SideMenuItem.prototype = {
     checkbox.setAttribute("tooltiptext", aAttachment.checkboxTooltip);
 
     if (aAttachment.checkboxState) {
       checkbox.setAttribute("checked", true);
     } else {
       checkbox.removeAttribute("checked");
     }
 
-    checkbox.addEventListener("command", function () {
+    // Stop the toggling of the checkbox from selecting the list item.
+    checkbox.addEventListener("mousedown", function (event) {
+      event.stopPropagation();
+    }, false);
+
+    checkbox.addEventListener("command", function (event) {
       ViewHelpers.dispatchEvent(checkbox, "check", {
         checked: checkbox.checked,
       });
     }, false);
 
     return checkbox;
   },
 
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -18,21 +18,34 @@ const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
-  "resource://gre/modules/devtools/NetworkHelper.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
-  "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+  "resource://gre/modules/devtools/Loader.jsm");
+
+Object.defineProperty(this, "WebConsoleUtils", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/utils").Utils;
+  },
+  configurable: true,
+  enumerable: true
+});
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  enumerable: true
+});
 
 this.EXPORTED_SYMBOLS = ["VariablesView"];
 
 /**
  * Debugger localization strings.
  */
 const STR = Services.strings.createBundle(DBG_STRINGS_URI);
 
--- a/browser/devtools/shared/widgets/VariablesViewController.jsm
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -7,17 +7,27 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
-Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+  "resource://gre/modules/devtools/Loader.jsm");
+
+Object.defineProperty(this, "WebConsoleUtils", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/utils").Utils;
+  },
+  configurable: true,
+  enumerable: true
+});
 
 XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
   Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
 );
 
 const MAX_LONG_STRING_LENGTH = 200000;
 
 this.EXPORTED_SYMBOLS = ["VariablesViewController"];
--- a/browser/devtools/styleeditor/StyleEditorDebuggee.jsm
+++ b/browser/devtools/styleeditor/StyleEditorDebuggee.jsm
@@ -118,17 +118,17 @@ StyleEditorDebuggee.prototype = {
   },
 
   /**
    * request baseURIObject information from the document
    */
   _getBaseURI: function() {
     let message = { type: "getBaseURI" };
     this._sendRequest(message, (response) => {
-      this.baseURI = response.baseURI;
+      this.baseURI = Services.io.newURI(response.baseURI, null, null);
     });
   },
 
   /**
    * Handler for document load, forward event with
    * all the stylesheets available on load.
    *
    * @param  {string} type
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -144,17 +144,17 @@ StyleEditorUI.prototype = {
     showFilePicker(file, false, parentWindow, onFileSelected);
   },
 
   /**
    * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
    */
   _onStyleSheetsCleared: function() {
     // remember selected sheet and line number for next load
-    if (this.selectedEditor) {
+    if (this.selectedEditor && this.selectedEditor.sourceEditor) {
       let href = this.selectedEditor.styleSheet.href;
       let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
       this.selectStyleSheet(href, line, col);
     }
 
     this._clearStyleSheetEditors();
     this._view.removeAll();
 
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1886,11 +1886,9 @@ XPCOMUtils.defineLazyGetter(this, "_stri
   return Services.strings.createBundle(
     "chrome://browser/locale/devtools/styleinspector.properties");
 });
 
 XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
-XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
-  return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
-});
+loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
--- a/browser/devtools/webconsole/Makefile.in
+++ b/browser/devtools/webconsole/Makefile.in
@@ -7,9 +7,9 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 include $(topsrcdir)/config/rules.mk
 
 libs::
-	$(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/framework
+	$(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/webconsole
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/console-output.js
@@ -0,0 +1,311 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Heritage = require("sdk/core/heritage");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+// Constants for compatibility with the Web Console output implementation before
+// bug 778766.
+// TODO: remove these once bug 778766 is fixed.
+const COMPAT = {
+  // The various categories of messages.
+  CATEGORIES: {
+    NETWORK: 0,
+    CSS: 1,
+    JS: 2,
+    WEBDEV: 3,
+    INPUT: 4,
+    OUTPUT: 5,
+    SECURITY: 6,
+  },
+
+  // The possible message severities.
+  SEVERITIES: {
+    ERROR: 0,
+    WARNING: 1,
+    INFO: 2,
+    LOG: 3,
+  },
+};
+
+/**
+ * The ConsoleOutput object is used to manage output of messages in the Web
+ * Console.
+ *
+ * @constructor
+ * @param object owner
+ *        The console output owner. This usually the WebConsoleFrame instance.
+ *        Any other object can be used, as long as it has the following
+ *        properties and methods:
+ *          - window
+ *          - document
+ *          - outputMessage(category, methodOrNode[, methodArguments])
+ *            TODO: this is needed temporarily, until bug 778766 is fixed.
+ */
+function ConsoleOutput(owner)
+{
+  this.owner = owner;
+  this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
+}
+
+ConsoleOutput.prototype = {
+  /**
+   * The document that holds the output.
+   * @type DOMDocument
+   */
+  get document() this.owner.document,
+
+  /**
+   * The DOM window that holds the output.
+   * @type Window
+   */
+  get window() this.owner.window,
+
+  /**
+   * Add a message to output.
+   *
+   * @param object ...args
+   *        Any number of Message objects.
+   * @return this
+   */
+  addMessage: function(...args)
+  {
+    for (let msg of args) {
+      msg.init(this);
+      this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage,
+                               [msg]);
+    }
+    return this;
+  },
+
+  /**
+   * Message renderer used for compatibility with the current Web Console output
+   * implementation. This method is invoked for every message object that is
+   * flushed to output. The message object is initialized and rendered, then it
+   * is displayed.
+   *
+   * TODO: remove this method once bug 778766 is fixed.
+   *
+   * @private
+   * @param object message
+   *        The message object to render.
+   * @return DOMElement
+   *         The message DOM element that can be added to the console output.
+   */
+  _onFlushOutputMessage: function(message)
+  {
+    return message.render().element;
+  },
+
+  /**
+   * Destroy this ConsoleOutput instance.
+   */
+  destroy: function()
+  {
+    this.owner = null;
+  },
+}; // ConsoleOutput.prototype
+
+/**
+ * Message objects container.
+ * @type object
+ */
+let Messages = {};
+
+/**
+ * The BaseMessage object is used for all types of messages. Every kind of
+ * message should use this object as its base.
+ *
+ * @constructor
+ */
+Messages.BaseMessage = function()
+{
+  this.widgets = new Set();
+};
+
+Messages.BaseMessage.prototype = {
+  /**
+   * Reference to the ConsoleOutput owner.
+   *
+   * @type object|null
+   *       This is |null| if the message is not yet initialized.
+   */
+  output: null,
+
+  /**
+   * Reference to the parent message object, if this message is in a group or if
+   * it is otherwise owned by another message.
+   *
+   * @type object|null
+   */
+  parent: null,
+
+  /**
+   * Message DOM element.
+   *
+   * @type DOMElement|null
+   *       This is |null| if the message is not yet rendered.
+   */
+  element: null,
+
+  /**
+   * Tells if this message is visible or not.
+   * @type boolean
+   */
+  get visible() {
+    return this.element && this.element.parentNode;
+  },
+
+  /**
+   * Holds the text-only representation of the message.
+   * @type string
+   */
+  textContent: "",
+
+  /**
+   * Set of widgets included in this message.
+   * @type Set
+   */
+  widgets: null,
+
+  // Properties that allow compatibility with the current Web Console output
+  // implementation.
+  _elementClassCompat: "",
+  _categoryCompat: null,
+  _severityCompat: null,
+
+  /**
+   * Initialize the message.
+   *
+   * @param object output
+   *        The ConsoleOutput owner.
+   * @param object [parent=null]
+   *        Optional: a different message object that owns this instance.
+   * @return this
+   */
+  init: function(output, parent=null)
+  {
+    this.output = output;
+    this.parent = parent;
+    return this;
+  },
+
+  /**
+   * Render the message. After this method is invoked the |element| property
+   * will point to the DOM element of this message.
+   * @return this
+   */
+  render: function()
+  {
+    if (!this.element) {
+      this.element = this._renderCompat();
+    }
+    return this;
+  },
+
+  /**
+   * Prepare the message container for the Web Console, such that it is
+   * compatible with the current implementation.
+   * TODO: remove this once bug 778766.
+   */
+  _renderCompat: function()
+  {
+    let doc = this.output.document;
+    let container = doc.createElementNS(XUL_NS, "richlistitem");
+    container.setAttribute("id", "console-msg-" + gSequenceId());
+    container.setAttribute("class", "hud-msg-node " + this._elementClassCompat);
+    container.category = this._categoryCompat;
+    container.severity = this._severityCompat;
+    container.clipboardText = this.textContent;
+    container.timestamp = this.timestamp;
+    container._messageObject = this;
+
+    let body = doc.createElementNS(XUL_NS, "description");
+    body.flex = 1;
+    body.classList.add("webconsole-msg-body");
+    container.appendChild(body);
+
+    return container;
+  },
+}; // Messages.BaseMessage.prototype
+
+
+/**
+ * The NavigationMarker is used to show a page load event.
+ *
+ * @constructor
+ * @extends Messages.BaseMessage
+ * @param string url
+ *        The URL to display.
+ * @param number timestamp
+ *        The message date and time, milliseconds elapsed since 1 January 1970
+ *        00:00:00 UTC.
+ */
+Messages.NavigationMarker = function(url, timestamp)
+{
+  Messages.BaseMessage.apply(this, arguments);
+  this._url = url;
+  this.textContent = "------ " + url;
+  this.timestamp = timestamp;
+};
+
+Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype,
+{
+  /**
+   * Message timestamp.
+   *
+   * @type number
+   *       Milliseconds elapsed since 1 January 1970 00:00:00 UTC.
+   */
+  timestamp: 0,
+
+  // Class names in order: category, severity then the class for the filter.
+  _elementClassCompat: "webconsole-msg-network webconsole-msg-info hud-networkinfo",
+  _categoryCompat: COMPAT.CATEGORIES.NETWORK,
+  _severityCompat: COMPAT.SEVERITIES.LOG,
+
+  /**
+   * Prepare the DOM element for this message.
+   * @return this
+   */
+  render: function()
+  {
+    if (this.element) {
+      return this;
+    }
+
+    let url = this._url;
+    let pos = url.indexOf("?");
+    if (pos > -1) {
+      url = url.substr(0, pos);
+    }
+
+    let doc = this.output.document;
+    let urlnode = doc.createElementNS(XHTML_NS, "span");
+    urlnode.className = "url";
+    urlnode.textContent = url;
+
+    // Add the text in the xul:description.webconsole-msg-body element.
+    let render = Messages.BaseMessage.prototype.render.bind(this);
+    render().element.firstChild.appendChild(urlnode);
+    this.element.classList.add("navigation-marker");
+    this.element.url = this._url;
+
+    return this;
+  },
+}); // Messages.NavigationMarker.prototype
+
+
+function gSequenceId()
+{
+  return gSequenceId.n++;
+}
+gSequenceId.n = 0;
+
+exports.ConsoleOutput = ConsoleOutput;
+exports.Messages = Messages;
rename from browser/devtools/webconsole/HUDService.jsm
rename to browser/devtools/webconsole/hudservice.js
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/hudservice.js
@@ -1,82 +1,68 @@
 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+const {Cc, Ci, Cu} = require("chrome");
 
-XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
-    "resource:///modules/devtools/gDevTools.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "devtools",
-    "resource://gre/modules/devtools/Loader.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-    "resource://gre/modules/Services.jsm");
+let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
+let Heritage = require("sdk/core/heritage");
 
-XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
-  "resource://gre/modules/devtools/dbg-server.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
-  "resource://gre/modules/devtools/dbg-client.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
-    "resource://gre/modules/devtools/WebConsoleUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "promise",
-    "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Heritage",
-    "resource:///modules/devtools/ViewHelpers.jsm");
-
-let Telemetry = devtools.require("devtools/shared/telemetry");
+loader.lazyGetter(this, "promise", () => require("sdk/core/promise"));
+loader.lazyGetter(this, "Telemetry", () => require("devtools/shared/telemetry"));
+loader.lazyGetter(this, "WebConsoleFrame", () => require("devtools/webconsole/webconsole").WebConsoleFrame);
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
+loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
+loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
+loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
 
 const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
 
 // The preference prefix for all of the Browser Console filters.
 const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
 
-this.EXPORTED_SYMBOLS = ["HUDService"];
-
 ///////////////////////////////////////////////////////////////////////////
 //// The HUD service
 
 function HUD_SERVICE()
 {
-  this.hudReferences = {};
+  this.consoles = new Map();
+  this.lastFinishedRequest = { callback: null };
 }
 
 HUD_SERVICE.prototype =
 {
+  _browserConsoleID: null,
+  _browserConsoleDefer: null,
+
   /**
-   * Keeps a reference for each HeadsUpDisplay that is created
-   * @type object
+   * Keeps a reference for each Web Console / Browser Console that is created.
+   * @type Map
    */
-  hudReferences: null,
+  consoles: null,
 
   /**
-   * getter for UI commands to be used by the frontend
+   * Assign a function to this property to listen for every request that
+   * completes. Used by unit tests. The callback takes one argument: the HTTP
+   * activity object as received from the remote Web Console.
    *
-   * @returns object
+   * @type object
+   *       Includes a property named |callback|. Assign the function to the
+   *       |callback| property of this object.
    */
-  get consoleUI() {
-    return HeadsUpDisplayUICommands;
-  },
+  lastFinishedRequest: null,
 
   /**
    * Firefox-specific current tab getter
    *
    * @returns nsIDOMWindow
    */
   currentContext: function HS_currentContext() {
     return Services.wm.getMostRecentWindow("navigator:browser");
@@ -95,91 +81,206 @@ HUD_SERVICE.prototype =
    *        The window of the web console owner.
    * @return object
    *         A promise object for the opening of the new WebConsole instance.
    */
   openWebConsole:
   function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow)
   {
     let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow);
-    this.hudReferences[hud.hudId] = hud;
+    this.consoles.set(hud.hudId, hud);
     return hud.init();
   },
 
   /**
    * Open a Browser Console for the given target.
    *
-   * @see devtools/framework/Target.jsm for details about targets.
+   * @see devtools/framework/target.js for details about targets.
    *
    * @param object aTarget
    *        The target that the browser console will connect to.
    * @param nsIDOMWindow aIframeWindow
    *        The window where the browser console UI is already loaded.
    * @param nsIDOMWindow aChromeWindow
    *        The window of the browser console owner.
    * @return object
    *         A promise object for the opening of the new BrowserConsole instance.
    */
   openBrowserConsole:
   function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow)
   {
     let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow);
-    this.hudReferences[hud.hudId] = hud;
+    this.consoles.set(hud.hudId, hud);
     return hud.init();
   },
 
   /**
-   * Returns the HeadsUpDisplay object associated to a content window.
+   * Returns the Web Console object associated to a content window.
    *
    * @param nsIDOMWindow aContentWindow
    * @returns object
    */
   getHudByWindow: function HS_getHudByWindow(aContentWindow)
   {
-    for each (let hud in this.hudReferences) {
+    for (let [hudId, hud] of this.consoles) {
       let target = hud.target;
       if (target && target.tab && target.window === aContentWindow) {
         return hud;
       }
     }
     return null;
   },
 
   /**
-   * Returns the hudId that is corresponding to the hud activated for the
-   * passed aContentWindow. If there is no matching hudId null is returned.
-   *
-   * @param nsIDOMWindow aContentWindow
-   * @returns string or null
-   */
-  getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow)
-  {
-    let hud = this.getHudByWindow(aContentWindow);
-    return hud ? hud.hudId : null;
-  },
-
-  /**
-   * Returns the hudReference for a given id.
+   * Returns the console instance for a given id.
    *
    * @param string aId
    * @returns Object
    */
   getHudReferenceById: function HS_getHudReferenceById(aId)
   {
-    return aId in this.hudReferences ? this.hudReferences[aId] : null;
+    return this.consoles.get(aId);
+  },
+
+  /**
+   * Toggle the Web Console for the current tab.
+   *
+   * @return object
+   *         A promise for either the opening of the toolbox that holds the Web
+   *         Console, or a Promise for the closing of the toolbox.
+   */
+  toggleWebConsole: function HS_toggleWebConsole()
+  {
+    let window = this.currentContext();
+    let target = devtools.TargetFactory.forTab(window.gBrowser.selectedTab);
+    let toolbox = gDevTools.getToolbox(target);
+
+    return toolbox && toolbox.currentToolId == "webconsole" ?
+        toolbox.destroy() :
+        gDevTools.showToolbox(target, "webconsole");
+  },
+
+  /**
+   * Find if there is a Web Console open for the current tab and return the
+   * instance.
+   * @return object|null
+   *         The WebConsole object or null if the active tab has no open Web
+   *         Console.
+   */
+  getOpenWebConsole: function HS_getOpenWebConsole()
+  {
+    let tab = this.currentContext().gBrowser.selectedTab;
+    if (!tab || !devtools.TargetFactory.isKnownTab(tab)) {
+      return null;
+    }
+    let target = devtools.TargetFactory.forTab(tab);
+    let toolbox = gDevTools.getToolbox(target);
+    let panel = toolbox ? toolbox.getPanel("webconsole") : null;
+    return panel ? panel.hud : null;
   },
 
   /**
-   * Assign a function to this property to listen for every request that
-   * completes. Used by unit tests. The callback takes one argument: the HTTP
-   * activity object as received from the remote Web Console.
+   * Toggle the Browser Console.
+   */
+  toggleBrowserConsole: function HS_toggleBrowserConsole()
+  {
+    if (this._browserConsoleID) {
+      let hud = this.getHudReferenceById(this._browserConsoleID);
+      return hud.destroy();
+    }
+
+    if (this._browserConsoleDefer) {
+      return this._browserConsoleDefer.promise;
+    }
+
+    this._browserConsoleDefer = promise.defer();
+
+    function connect()
+    {
+      let deferred = promise.defer();
+
+      if (!DebuggerServer.initialized) {
+        DebuggerServer.init();
+        DebuggerServer.addBrowserActors();
+      }
+
+      let client = new DebuggerClient(DebuggerServer.connectPipe());
+      client.connect(() =>
+        client.listTabs((aResponse) => {
+          // Add Global Process debugging...
+          let globals = JSON.parse(JSON.stringify(aResponse));
+          delete globals.tabs;
+          delete globals.selected;
+          // ...only if there are appropriate actors (a 'from' property will
+          // always be there).
+          if (Object.keys(globals).length > 1) {
+            deferred.resolve({ form: globals, client: client, chrome: true });
+          } else {
+            deferred.reject("Global console not found!");
+          }
+        }));
+
+      return deferred.promise;
+    }
+
+    let target;
+    function getTarget(aConnection)
+    {
+      let options = {
+        form: aConnection.form,
+        client: aConnection.client,
+        chrome: true,
+      };
+
+      return devtools.TargetFactory.forRemoteTab(options);
+    }
+
+    function openWindow(aTarget)
+    {
+      target = aTarget;
+
+      let deferred = promise.defer();
+
+      let win = Services.ww.openWindow(null, devtools.Tools.webConsole.url, "_blank",
+                                       BROWSER_CONSOLE_WINDOW_FEATURES, null);
+      win.addEventListener("DOMContentLoaded", function onLoad() {
+        win.removeEventListener("DOMContentLoaded", onLoad);
+
+        // Set the correct Browser Console title.
+        let root = win.document.documentElement;
+        root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
+
+        deferred.resolve(win);
+      });
+
+      return deferred.promise;
+    }
+
+    connect().then(getTarget).then(openWindow).then((aWindow) =>
+      this.openBrowserConsole(target, aWindow, aWindow)
+        .then((aBrowserConsole) => {
+          this._browserConsoleID = aBrowserConsole.hudId;
+          this._browserConsoleDefer.resolve(aBrowserConsole);
+          this._browserConsoleDefer = null;
+        }));
+
+    return this._browserConsoleDefer.promise;
+  },
+
+  /**
+   * Get the Browser Console instance, if open.
    *
-   * @type function
+   * @return object|null
+   *         A BrowserConsole instance or null if the Browser Console is not
+   *         open.
    */
-  lastFinishedRequestCallback: null,
+  getBrowserConsole: function HS_getBrowserConsole()
+  {
+    return this.getHudReferenceById(this._browserConsoleID);
+  },
 };
 
 
 /**
  * A WebConsole instance is an interactive console initialized *per target*
  * that displays console log data as well as provides an interactive terminal to
  * manipulate the target's document content.
  *
@@ -204,36 +305,37 @@ function WebConsole(aTarget, aIframeWind
 
   this.browserWindow = this.chromeWindow.top;
 
   let element = this.browserWindow.document.documentElement;
   if (element.getAttribute("windowtype") != "navigator:browser") {
     this.browserWindow = HUDService.currentContext();
   }
 
-  this.ui = new this.iframeWindow.WebConsoleFrame(this);
+  this.ui = new WebConsoleFrame(this);
 }
 
 WebConsole.prototype = {
   iframeWindow: null,
   chromeWindow: null,
   browserWindow: null,
   hudId: null,
   target: null,
   ui: null,
   _browserConsole: false,
   _destroyer: null,
 
   /**
-   * Getter for HUDService.lastFinishedRequestCallback.
+   * Getter for a function to to listen for every request that completes. Used
+   * by unit tests. The callback takes one argument: the HTTP activity object as
+   * received from the remote Web Console.
    *
-   * @see HUDService.lastFinishedRequestCallback
    * @type function
    */
-  get lastFinishedRequestCallback() HUDService.lastFinishedRequestCallback,
+  get lastFinishedRequestCallback() HUDService.lastFinishedRequest.callback,
 
   /**
    * Getter for the xul:popupset that holds any popups we open.
    * @type nsIDOMElement
    */
   get mainPopupSet()
   {
     return this.browserWindow.document.getElementById("mainPopupSet");
@@ -466,17 +568,17 @@ WebConsole.prototype = {
    *         A promise object that is resolved once the Web Console is closed.
    */
   destroy: function WC_destroy()
   {
     if (this._destroyer) {
       return this._destroyer.promise;
     }
 
-    delete HUDService.hudReferences[this.hudId];
+    HUDService.consoles.delete(this.hudId);
 
     this._destroyer = promise.defer();
 
     let popupset = this.mainPopupSet;
     let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
     for (let panel of panels) {
       panel.hidePopup();
     }
@@ -587,158 +689,30 @@ BrowserConsole.prototype = Heritage.exte
 
     this._telemetry.toolClosed("browserconsole");
 
     this._bc_destroyer = promise.defer();
 
     let chromeWindow = this.chromeWindow;
     this.$destroy().then(() =>
       this.target.client.close(() => {
-        HeadsUpDisplayUICommands._browserConsoleID = null;
+        HUDService._browserConsoleID = null;
         chromeWindow.close();
         this._bc_destroyer.resolve(null);
       }));
 
     return this._bc_destroyer.promise;
   },
 });
 
-
-//////////////////////////////////////////////////////////////////////////
-// HeadsUpDisplayUICommands
-//////////////////////////////////////////////////////////////////////////
-
-var HeadsUpDisplayUICommands = {
-  _browserConsoleID: null,
-  _browserConsoleDefer: null,
-
-  /**
-   * Toggle the Web Console for the current tab.
-   *
-   * @return object
-   *         A promise for either the opening of the toolbox that holds the Web
-   *         Console, or a promise for the closing of the toolbox.
-   */
-  toggleHUD: function UIC_toggleHUD()
-  {
-    let window = HUDService.currentContext();
-    let target = devtools.TargetFactory.forTab(window.gBrowser.selectedTab);
-    let toolbox = gDevTools.getToolbox(target);
-
-    return toolbox && toolbox.currentToolId == "webconsole" ?
-        toolbox.destroy() :
-        gDevTools.showToolbox(target, "webconsole");
-  },
-
-  /**
-   * Find if there is a Web Console open for the current tab and return the
-   * instance.
-   * @return object|null
-   *         The WebConsole object or null if the active tab has no open Web
-   *         Console.
-   */
-  getOpenHUD: function UIC_getOpenHUD()
-  {
-    let tab = HUDService.currentContext().gBrowser.selectedTab;
-    if (!tab || !devtools.TargetFactory.isKnownTab(tab)) {
-      return null;
-    }
-    let target = devtools.TargetFactory.forTab(tab);
-    let toolbox = gDevTools.getToolbox(target);
-    let panel = toolbox ? toolbox.getPanel("webconsole") : null;
-    return panel ? panel.hud : null;
-  },
-
-  /**
-   * Toggle the Browser Console.
-   */
-  toggleBrowserConsole: function UIC_toggleBrowserConsole()
-  {
-    if (this._browserConsoleID) {
-      let hud = HUDService.getHudReferenceById(this._browserConsoleID);
-      return hud.destroy();
-    }
-
-    if (this._browserConsoleDefer) {
-      return this._browserConsoleDefer.promise;
-    }
-
-    this._browserConsoleDefer = promise.defer();
-
-    function connect()
-    {
-      let deferred = promise.defer();
-
-      if (!DebuggerServer.initialized) {
-        DebuggerServer.init();
-        DebuggerServer.addBrowserActors();
-      }
-
-      let client = new DebuggerClient(DebuggerServer.connectPipe());
-      client.connect(() =>
-        client.listTabs((aResponse) => {
-          // Add Global Process debugging...
-          let globals = JSON.parse(JSON.stringify(aResponse));
-          delete globals.tabs;
-          delete globals.selected;
-          // ...only if there are appropriate actors (a 'from' property will
-          // always be there).
-          if (Object.keys(globals).length > 1) {
-            deferred.resolve({ form: globals, client: client, chrome: true });
-          } else {
-            deferred.reject("Global console not found!");
-          }
-        }));
-
-      return deferred.promise;
-    }
-
-    let target;
-    function getTarget(aConnection)
-    {
-      let options = {
-        form: aConnection.form,
-        client: aConnection.client,
-        chrome: true,
-      };
-
-      return devtools.TargetFactory.forRemoteTab(options);
-    }
-
-    function openWindow(aTarget)
-    {
-      target = aTarget;
-
-      let deferred = promise.defer();
-
-      let win = Services.ww.openWindow(null, devtools.Tools.webConsole.url, "_blank",
-                                       BROWSER_CONSOLE_WINDOW_FEATURES, null);
-      win.addEventListener("DOMContentLoaded", function onLoad() {
-        win.removeEventListener("DOMContentLoaded", onLoad);
-
-        // Set the correct Browser Console title.
-        let root = win.document.documentElement;
-        root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
-
-        deferred.resolve(win);
-      });
-
-      return deferred.promise;
-    }
-
-    connect().then(getTarget).then(openWindow).then((aWindow) =>
-      HUDService.openBrowserConsole(target, aWindow, aWindow)
-        .then((aBrowserConsole) => {
-          this._browserConsoleID = aBrowserConsole.hudId;
-          this._browserConsoleDefer.resolve(aBrowserConsole);
-          this._browserConsoleDefer = null;
-        }));
-
-    return this._browserConsoleDefer.promise;
-  },
-
-  get browserConsole() {
-    return HUDService.getHudReferenceById(this._browserConsoleID);
-  },
-};
-
 const HUDService = new HUD_SERVICE();
 
+(() => {
+  let methods = ["openWebConsole", "openBrowserConsole", "toggleWebConsole",
+                 "toggleBrowserConsole", "getOpenWebConsole",
+                 "getBrowserConsole", "getHudByWindow", "getHudReferenceById"];
+  for (let method of methods) {
+    exports[method] = HUDService[method].bind(HUDService);
+  }
+
+  exports.consoles = HUDService.consoles;
+  exports.lastFinishedRequest = HUDService.lastFinishedRequest;
+})();
--- a/browser/devtools/webconsole/moz.build
+++ b/browser/devtools/webconsole/moz.build
@@ -1,14 +1,7 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['test']
-
-EXTRA_JS_MODULES += [
-    'HUDService.jsm',
-    'NetworkPanel.jsm',
-    'WebConsolePanel.jsm',
-]
-
rename from browser/devtools/webconsole/NetworkPanel.jsm
rename to browser/devtools/webconsole/network-panel.js
--- a/browser/devtools/webconsole/NetworkPanel.jsm
+++ b/browser/devtools/webconsole/network-panel.js
@@ -1,52 +1,40 @@
 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1",
-                                   "nsIMIMEService");
+const {Cc, Ci, Cu} = require("chrome");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
-                                  "resource://gre/modules/devtools/NetworkHelper.jsm");
+loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+loader.lazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
-                                  "resource://gre/modules/devtools/WebConsoleUtils.jsm");
+let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
 
-this.EXPORTED_SYMBOLS = ["NetworkPanel"];
 
 /**
  * Creates a new NetworkPanel.
  *
  * @constructor
  * @param nsIDOMNode aParent
  *        Parent node to append the created panel to.
  * @param object aHttpActivity
  *        HttpActivity to display in the panel.
  * @param object aWebConsoleFrame
  *        The parent WebConsoleFrame object that owns this network panel
  *        instance.
  */
-this.NetworkPanel =
 function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame)
 {
   let doc = aParent.ownerDocument;
   this.httpActivity = aHttpActivity;
   this.webconsole = aWebConsoleFrame;
   this._longStringClick = this._longStringClick.bind(this);
   this._responseBodyFetch = this._responseBodyFetch.bind(this);
   this._requestBodyFetch = this._requestBodyFetch.bind(this);
@@ -103,16 +91,17 @@ function NetworkPanel(aParent, aHttpActi
   let footer = createElement(doc, "hbox", { align: "end" });
   createAndAppendElement(footer, "spacer", { flex: 1 });
 
   createAndAppendElement(footer, "resizer", { dir: "bottomend" });
   this.panel.appendChild(footer);
 
   aParent.appendChild(this.panel);
 }
+exports.NetworkPanel = NetworkPanel;
 
 NetworkPanel.prototype =
 {
   /**
    * The current state of the output.
    */
   _state: 0,
 
rename from browser/devtools/webconsole/WebConsolePanel.jsm
rename to browser/devtools/webconsole/panel.js
--- a/browser/devtools/webconsole/WebConsolePanel.jsm
+++ b/browser/devtools/webconsole/panel.js
@@ -1,37 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = [ "WebConsolePanel" ];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+const {Cc, Ci, Cu} = require("chrome");
 
-XPCOMUtils.defineLazyModuleGetter(this, "promise",
-    "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise");
-
-XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
-    "resource:///modules/HUDService.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
-    "resource:///modules/devtools/shared/event-emitter.js");
+loader.lazyGetter(this, "promise", () => require("sdk/core/promise"));
+loader.lazyGetter(this, "HUDService", () => require("devtools/webconsole/hudservice"));
+loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
 
 /**
  * A DevToolPanel that controls the Web Console.
  */
-function WebConsolePanel(iframeWindow, toolbox) {
+function WebConsolePanel(iframeWindow, toolbox)
+{
   this._frameWindow = iframeWindow;
   this._toolbox = toolbox;
   EventEmitter.decorate(this);
 }
+exports.WebConsolePanel = WebConsolePanel;
 
 WebConsolePanel.prototype = {
   hud: null,
 
   /**
    * Open is effectively an asynchronous constructor.
    *
    * @return object
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -12,17 +12,16 @@ include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_FILES = \
 	browser_webconsole_notifications.js \
 	browser_webconsole_message_node_id.js \
 	browser_webconsole_bug_580030_errors_after_page_reload.js \
 	browser_webconsole_basic_net_logging.js \
 	browser_webconsole_bug_579412_input_focus.js \
 	browser_webconsole_bug_580001_closing_after_completion.js \
-	browser_webconsole_bug_580400_groups.js \
 	browser_webconsole_bug_588730_text_node_insertion.js \
 	browser_webconsole_bug_601667_filter_buttons.js \
 	browser_webconsole_bug_597136_external_script_errors.js \
 	browser_webconsole_bug_597136_network_requests_from_chrome.js \
 	browser_webconsole_completion.js \
 	browser_webconsole_console_logging_api.js \
 	browser_webconsole_change_font_size.js \
 	browser_webconsole_chrome.js \
@@ -140,16 +139,19 @@ MOCHITEST_BROWSER_FILES = \
 	browser_console_clear_on_reload.js \
 	browser_console_keyboard_accessibility.js \
 	browser_console_filters.js \
 	browser_console_dead_objects.js \
 	browser_console_iframe_messages.js \
 	browser_console_variables_view_while_debugging_and_inspecting.js \
 	browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js \
 	browser_webconsole_cached_autocomplete.js \
+	browser_console_navigation_marker.js \
+	browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js \
+	browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js \
 	head.js \
 	$(NULL)
 
 ifeq ($(OS_ARCH), Darwin)
 MOCHITEST_BROWSER_FILES += \
 	browser_webconsole_bug_804845_ctrl_key_nav.js \
         $(NULL)
 endif
@@ -239,15 +241,19 @@ MOCHITEST_BROWSER_FILES += \
 	test-bug-821877-csperrors.html^headers^ \
 	test-bug-846918-hsts-invalid-headers.html \
 	test-bug-846918-hsts-invalid-headers.html^headers^ \
 	test-eval-in-stackframe.html \
 	test-bug-859170-longstring-hang.html \
 	test-bug-837351-security-errors.html \
 	test-bug-869003-top-window.html \
 	test-bug-869003-iframe.html \
+	test-iframe-762593-insecure-form-action.html \
+	test-iframe-762593-insecure-frame.html \
+	test-bug-762593-insecure-passwords-web-console-warning.html \
+	test-bug-762593-insecure-passwords-about-blank-web-console-warning.html \
 	test-consoleiframes.html \
 	test-iframe1.html \
 	test-iframe2.html \
 	test-iframe3.html \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
+++ b/browser/devtools/webconsole/test/browser_bug_871156_ctrlw_close_tab.js
@@ -5,62 +5,79 @@
 
 // Check that Ctrl-W closes the Browser Console and that Ctrl-W closes the
 // current tab when using the Web Console - bug 871156.
 
 function test()
 {
   const TEST_URI = "data:text/html;charset=utf8,<title>bug871156</title>\n" +
                    "<p>hello world";
+  let firstTab = gBrowser.selectedTab;
+  Services.prefs.setBoolPref("browser.tabs.animate", false);
+
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 
   function consoleOpened(hud)
   {
     ok(hud, "Web Console opened");
 
-    let tabClosed = false, toolboxDestroyed = false;
+    let tabClosed = promise.defer();
+    let toolboxDestroyed = promise.defer();
+    let tabSelected = promise.defer();
+
+    let pageWindow = firstTab.linkedBrowser.contentWindow;
+    let toolbox = gDevTools.getToolbox(hud.target);
 
     gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
       gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
-
-      ok(true, "tab closed");
+      info("tab closed");
+      tabClosed.resolve(null);
+    });
 
-      tabClosed = true;
-      if (toolboxDestroyed) {
-        testBrowserConsole();
+    gBrowser.tabContainer.addEventListener("TabSelect", function onTabSelect() {
+      gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
+      if (gBrowser.selectedTab == firstTab) {
+        info("tab selected");
+        tabSelected.resolve(null);
       }
     });
 
-    let toolbox = gDevTools.getToolbox(hud.target);
     toolbox.once("destroyed", () => {
-      ok(true, "toolbox destroyed");
-
-      toolboxDestroyed = true;
-      if (tabClosed) {
-        testBrowserConsole();
-      }
+      info("toolbox destroyed");
+      toolboxDestroyed.resolve(null);
     });
 
-    EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+    promise.all([tabClosed.promise, toolboxDestroyed.promise, tabSelected.promise ]).then(() => {
+      info("promise.all resolved");
+      waitForFocus(testBrowserConsole, pageWindow, true);
+    });
+
+    // Get out of the web console initialization.
+    executeSoon(() => {
+      EventUtils.synthesizeKey("w", { accelKey: true });
+    });
   }
 
   function testBrowserConsole()
   {
     info("test the Browser Console");
 
-    HUDConsoleUI.toggleBrowserConsole().then((hud) => {
+    HUDService.toggleBrowserConsole().then((hud) => {
       ok(hud, "Browser Console opened");
 
       Services.obs.addObserver(function onDestroy() {
         Services.obs.removeObserver(onDestroy, "web-console-destroyed");
         ok(true, "the Browser Console closed");
 
-        executeSoon(finishTest);
+        Services.prefs.clearUserPref("browser.tabs.animate");
+        waitForFocus(finish, content, true);
       }, "web-console-destroyed", false);
 
-      EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+      waitForFocus(() => {
+        EventUtils.synthesizeKey("w", { accelKey: true }, hud.iframeWindow);
+      }, hud.iframeWindow);
     });
   }
 }
--- a/browser/devtools/webconsole/test/browser_console.js
+++ b/browser/devtools/webconsole/test/browser_console.js
@@ -4,26 +4,31 @@
  */
 
 // Test the basic features of the Browser Console, bug 587757.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html?" + Date.now();
 
 function test()
 {
-  let oldFunction = HUDConsoleUI.toggleBrowserConsole;
-  let functionExecuted = false;
-  HUDConsoleUI.toggleBrowserConsole = () => functionExecuted = true;
-  EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, content);
+  Services.obs.addObserver(function observer(aSubject) {
+    Services.obs.removeObserver(observer, "web-console-created");
+    aSubject.QueryInterface(Ci.nsISupportsString);
+
+    let hud = HUDService.getBrowserConsole();
+    ok(hud, "browser console is open");
+    is(aSubject.data, hud.hudId, "notification hudId is correct");
 
-  ok(functionExecuted,
-     "toggleBrowserConsole() was executed by the Ctrl-Shift-J key shortcut");
+    executeSoon(() => consoleOpened(hud));
+  }, "web-console-created", false);
 
-  HUDConsoleUI.toggleBrowserConsole = oldFunction;
-  HUDConsoleUI.toggleBrowserConsole().then(consoleOpened);
+  let hud = HUDService.getBrowserConsole();
+  ok(!hud, "browser console is not open");
+  info("wait for the browser console to open with ctrl-shift-j");
+  EventUtils.synthesizeKey("j", { accelKey: true, shiftKey: true }, content);
 }
 
 function consoleOpened(hud)
 {
   hud.jsterm.clearOutput(true);
 
   expectUncaughtException();
   executeSoon(() => {
--- a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
+++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -19,17 +19,17 @@ function test()
 
     openConsole(null, consoleOpened);
   }, true);
 
   function consoleOpened(hud)
   {
     ok(hud, "web console opened");
     webconsole = hud;
-    HUDConsoleUI.toggleBrowserConsole().then(browserConsoleOpened);
+    HUDService.toggleBrowserConsole().then(browserConsoleOpened);
   }
 
   function browserConsoleOpened(hud)
   {
     ok(hud, "browser console opened");
     browserconsole = hud;
 
     // Cause an exception in a script loaded with the addon-sdk loader.
--- a/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
+++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
@@ -8,17 +8,17 @@
 function test()
 {
   let storage = Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", {}).ConsoleAPIStorage;
   storage.clearEvents();
 
   let console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console;
   console.log("bug861338-log-cached");
 
-  HUDConsoleUI.toggleBrowserConsole().then(consoleOpened);
+  HUDService.toggleBrowserConsole().then(consoleOpened);
   let hud = null;
 
   function consoleOpened(aHud)
   {
     hud = aHud;
     waitForMessages({
       webconsole: hud,
       messages: [{
--- a/browser/devtools/webconsole/test/browser_console_dead_objects.js
+++ b/browser/devtools/webconsole/test/browser_console_dead_objects.js
@@ -10,17 +10,17 @@ const TEST_URI = "data:text/html;charset
 function test()
 {
   let hud = null;
 
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     info("open the browser console");
-    HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+    HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
   }, true);
 
   function onBrowserConsoleOpen(aHud)
   {
     hud = aHud;
     ok(hud, "browser console opened");
 
     hud.jsterm.clearOutput();
--- a/browser/devtools/webconsole/test/browser_console_error_source_click.js
+++ b/browser/devtools/webconsole/test/browser_console_error_source_click.js
@@ -11,17 +11,17 @@ const TEST_URI = "data:text/html;charset
                  "style='test-color: green-please'>click!</button>";
 function test()
 {
   let hud;
 
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
-    HUDConsoleUI.toggleBrowserConsole().then(browserConsoleOpened);
+    HUDService.toggleBrowserConsole().then(browserConsoleOpened);
   }, true);
 
   function browserConsoleOpened(aHud)
   {
     hud = aHud;
     ok(hud, "browser console opened");
 
     let button = content.document.querySelector("button");
--- a/browser/devtools/webconsole/test/browser_console_filters.js
+++ b/browser/devtools/webconsole/test/browser_console_filters.js
@@ -40,17 +40,17 @@ function consoleOpened(hud)
   hud.setFilterState("exception", true);
 
   executeSoon(() => closeConsole(null, onWebConsoleClose));
 }
 
 function onWebConsoleClose()
 {
   info("web console closed");
-  HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+  HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
 }
 
 function onBrowserConsoleOpen(hud)
 {
   ok(hud, "browser console opened");
 
   is(Services.prefs.getBoolPref(BROWSER_CONSOLE_PREFIX + "exception"), true,
      "'exception' filter is enabled (browser console)");
--- a/browser/devtools/webconsole/test/browser_console_iframe_messages.js
+++ b/browser/devtools/webconsole/test/browser_console_iframe_messages.js
@@ -80,17 +80,17 @@ function consoleOpened(hud)
       closeConsole(null, onWebConsoleClose);
     });
   });
 }
 
 function onWebConsoleClose()
 {
   info("web console closed");
-  HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+  HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
 }
 
 function onBrowserConsoleOpen(hud)
 {
   ok(hud, "browser console opened");
   waitForMessages({
     webconsole: hud,
     messages: expectedMessages,
--- a/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
+++ b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
@@ -49,21 +49,24 @@ function test()
     info("try ctrl-f to focus filter");
     EventUtils.synthesizeKey("F", { accelKey: true });
     ok(!hud.jsterm.inputNode.getAttribute("focused"),
        "jsterm input is not focused");
     is(hud.ui.filterBox.getAttribute("focused"), "true",
        "filter input is focused");
 
     if (Services.appinfo.OS == "Darwin") {
+      ok(hud.ui.getFilterState("network"), "network category is enabled");
       EventUtils.synthesizeKey("t", { ctrlKey: true });
+      ok(!hud.ui.getFilterState("network"), "accesskey for Network works");
+      EventUtils.synthesizeKey("t", { ctrlKey: true });
+      ok(hud.ui.getFilterState("network"), "accesskey for Network works (again)");
     }
     else {
       EventUtils.synthesizeKey("N", { altKey: true });
+      let net = hud.ui.document.querySelector("toolbarbutton[category=net]");
+      is(hud.ui.document.activeElement, net,
+         "accesskey for Network category focuses the Net button");
     }
 
-    let net = hud.ui.document.querySelector("toolbarbutton[category=net]");
-    is(hud.ui.document.activeElement, net,
-       "accesskey for Network category focuses the Net button");
-
     finishTest();
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_navigation_marker.js
@@ -0,0 +1,81 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Check that the navigation marker shows on page reload - bug 793996.
+
+function test()
+{
+  const PREF = "devtools.webconsole.persistlog";
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+  let hud = null;
+  let Messages = require("devtools/webconsole/console-output").Messages;
+
+  Services.prefs.setBoolPref(PREF, true);
+  registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+  addTab(TEST_URI);
+
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+
+  function consoleOpened(aHud)
+  {
+    hud = aHud;
+    ok(hud, "Web Console opened");
+
+    hud.jsterm.clearOutput();
+    content.console.log("foobarz1");
+    waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "foobarz1",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      }],
+    }).then(onConsoleMessage);
+  }
+
+  function onConsoleMessage()
+  {
+    browser.addEventListener("load", onReload, true);
+    content.location.reload();
+  }
+
+  function onReload()
+  {
+    browser.removeEventListener("load", onReload, true);
+
+    content.console.log("foobarz2");
+
+    waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "page reload",
+        text: "test-console.html",
+        category: CATEGORY_NETWORK,
+        severity: SEVERITY_LOG,
+      },
+      {
+        text: "foobarz2",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      },
+      {
+        name: "navigation marker",
+        text: "test-console.html",
+        type: Messages.NavigationMarker,
+      }],
+    }).then(onConsoleMessageAfterReload);
+  }
+
+  function onConsoleMessageAfterReload()
+  {
+    isnot(hud.outputNode.textContent.indexOf("foobarz1"), -1,
+          "foobarz1 is still in the output");
+    finishTest();
+  }
+}
--- a/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js
+++ b/browser/devtools/webconsole/test/browser_console_nsiconsolemessage.js
@@ -46,17 +46,17 @@ function consoleOpened(hud)
        "nsIConsoleMessages are not displayed (confirmed)");
     closeConsole(null, onWebConsoleClose);
   });
 }
 
 function onWebConsoleClose()
 {
   info("web console closed");
-  HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+  HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
 }
 
 function onBrowserConsoleOpen(hud)
 {
   ok(hud, "browser console opened");
   Services.console.logStringMessage("test2 for bug859756");
 
   waitForMessages({
--- a/browser/devtools/webconsole/test/browser_console_private_browsing.js
+++ b/browser/devtools/webconsole/test/browser_console_private_browsing.js
@@ -123,17 +123,17 @@ function test()
     }).then(testBrowserConsole);
   }
 
   function testBrowserConsole()
   {
     info("testBrowserConsole()");
     closeConsole(privateTab, () => {
       info("web console closed");
-      privateWindow.HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleOpen);
+      privateWindow.HUDService.toggleBrowserConsole().then(onBrowserConsoleOpen);
     });
   }
 
   // Make sure that the cached messages from private tabs are not displayed in
   // the browser console.
   function checkNoPrivateMessages()
   {
     let text = hud.outputNode.textContent;
@@ -162,20 +162,20 @@ function test()
   {
     info("close the private window and check if the private messages are removed");
     hud.jsterm.once("private-messages-cleared", () => {
       isnot(hud.outputNode.textContent.indexOf("bug874061-not-private"), -1,
             "non-private messages are still shown after private window closed");
       checkNoPrivateMessages();
 
       info("close the browser console");
-      privateWindow.HUDConsoleUI.toggleBrowserConsole().then(() => {
+      privateWindow.HUDService.toggleBrowserConsole().then(() => {
         info("reopen the browser console");
         executeSoon(() =>
-          HUDConsoleUI.toggleBrowserConsole().then(onBrowserConsoleReopen));
+          HUDService.toggleBrowserConsole().then(onBrowserConsoleReopen));
       });
     });
     privateWindow.BrowserTryToCloseWindow();
   }
 
   function onBrowserConsoleReopen(aHud)
   {
     hud = aHud;
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* 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/. */
-
-// Tests that console groups behave properly.
-
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
-
-function test() {
-  addTab(TEST_URI);
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    openConsole(null, testGroups);
-  }, true);
-}
-
-function testGroups(HUD) {
-  let jsterm = HUD.jsterm;
-  let outputNode = HUD.outputNode;
-  jsterm.clearOutput();
-
-  // We test for one group by testing for zero "new" groups. The
-  // "webconsole-new-group" class creates a divider. Thus one group is
-  // indicated by zero new groups, two groups are indicated by one new group,
-  // and so on.
-
-  let waitForSecondMessage = {
-    name: "second console message",
-    validatorFn: function()
-    {
-      return outputNode.querySelectorAll(".webconsole-msg-output").length == 2;
-    },
-    successFn: function()
-    {
-      let timestamp1 = Date.now();
-      if (timestamp1 - timestamp0 < 5000) {
-        is(outputNode.querySelectorAll(".webconsole-new-group").length, 0,
-           "no group dividers exist after the second console message");
-      }
-
-      for (let i = 0; i < outputNode.itemCount; i++) {
-        outputNode.getItemAtIndex(i).timestamp = 0;   // a "far past" value
-      }
-
-      jsterm.execute("2");
-      waitForSuccess(waitForThirdMessage);
-    },
-    failureFn: finishTest,
-  };
-
-  let waitForThirdMessage = {
-    name: "one group divider exists after the third console message",
-    validatorFn: function()
-    {
-      return outputNode.querySelectorAll(".webconsole-new-group").length == 1;
-    },
-    successFn: finishTest,
-    failureFn: finishTest,
-  };
-
-  let timestamp0 = Date.now();
-  jsterm.execute("0");
-
-  waitForSuccess({
-    name: "no group dividers exist after the first console message",
-    validatorFn: function()
-    {
-      return outputNode.querySelectorAll(".webconsole-new-group").length == 0;
-    },
-    successFn: function()
-    {
-      jsterm.execute("1");
-      waitForSuccess(waitForSecondMessage);
-    },
-    failureFn: finishTest,
-  });
-}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
@@ -1,50 +1,49 @@
 /* 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/. */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-585956-console-trace.html";
 
 function test() {
-  addTab(TEST_URI);
+  addTab("data:text/html;charset=utf8,<p>hello");
   browser.addEventListener("load", tabLoaded, true);
-}
 
-function tabLoaded() {
-  browser.removeEventListener("load", tabLoaded, true);
+  function tabLoaded() {
+    browser.removeEventListener("load", tabLoaded, true);
 
-  openConsole(null, function(hud) {
-    content.location.reload();
+    openConsole(null, function(hud) {
+      content.location = TEST_URI;
 
-    waitForSuccess({
-      name: "stacktrace message",
-      validatorFn: function()
-      {
-        return hud.outputNode.querySelector(".hud-log");
-      },
-      successFn: performChecks,
-      failureFn: finishTest,
+      waitForMessages({
+        webconsole: hud,
+        messages: [{
+          name: "console.trace output",
+          consoleTrace: {
+            file: "test-bug-585956-console-trace.html",
+            fn: "window.foobar585956c",
+          },
+        }],
+      }).then(performChecks);
     });
-  });
+  }
+
+  function performChecks(results) {
+    let node = [...results[0].matched][0];
+
+    // The expected stack trace object.
+    let stacktrace = [
+      { filename: TEST_URI, lineNumber: 9, functionName: "window.foobar585956c", language: 2 },
+      { filename: TEST_URI, lineNumber: 14, functionName: "foobar585956b", language: 2 },
+      { filename: TEST_URI, lineNumber: 18, functionName: "foobar585956a", language: 2 },
+      { filename: TEST_URI, lineNumber: 21, functionName: null, language: 2 }
+    ];
+
+    ok(node, "found trace log node");
+    ok(node._stacktrace, "found stacktrace object");
+    is(node._stacktrace.toSource(), stacktrace.toSource(), "stacktrace is correct");
+    isnot(node.textContent.indexOf("bug-585956"), -1, "found file name");
+
+    finishTest();
+  }
 }
-
-function performChecks() {
-  // The expected stack trace object.
-  let stacktrace = [
-    { filename: TEST_URI, lineNumber: 9, functionName: "window.foobar585956c", language: 2 },
-    { filename: TEST_URI, lineNumber: 14, functionName: "foobar585956b", language: 2 },
-    { filename: TEST_URI, lineNumber: 18, functionName: "foobar585956a", language: 2 },
-    { filename: TEST_URI, lineNumber: 21, functionName: null, language: 2 }
-  ];
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let HUD = HUDService.hudReferences[hudId];
-
-  let node = HUD.outputNode.querySelector(".hud-log");
-  ok(node, "found trace log node");
-  ok(node._stacktrace, "found stacktrace object");
-  is(node._stacktrace.toSource(), stacktrace.toSource(), "stacktrace is correct");
-  isnot(node.textContent.indexOf("bug-585956"), -1, "found file name");
-
-  finishTest();
-}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
@@ -20,43 +20,44 @@ function tabLoaded(aEvent) {
     expectUncaughtException();
     content.location.reload();
   });
 }
 
 function tabReloaded(aEvent) {
   gBrowser.selectedBrowser.removeEventListener(aEvent.type, tabReloaded, true);
 
-  let hudId = HUDService.getHudIdByWindow(content);
-  let HUD = HUDService.hudReferences[hudId];
+  let HUD = HUDService.getHudByWindow(content);
   ok(HUD, "Web Console is open");
 
-  waitForSuccess({
-    name: "error message displayed",
-    validatorFn: function() {
-      return HUD.outputNode.textContent.indexOf("fooBug597756_error") > -1;
-    },
-    successFn: function() {
-      if (newTabIsOpen) {
-        finishTest();
-        return;
-      }
-      closeConsole(gBrowser.selectedTab, function() {
-        gBrowser.removeCurrentTab();
+  waitForMessages({
+    webconsole: HUD,
+    messages: [{
+      name: "error message displayed",
+      text: "fooBug597756_error",
+      category: CATEGORY_JS,
+      severity: SEVERITY_ERROR,
+    }],
+  }).then(() => {
+    if (newTabIsOpen) {
+      finishTest();
+      return;
+    }
 
-        let newTab = gBrowser.addTab();
-        gBrowser.selectedTab = newTab;
+    closeConsole(gBrowser.selectedTab, () => {
+      gBrowser.removeCurrentTab();
+
+      let newTab = gBrowser.addTab();
+      gBrowser.selectedTab = newTab;
 
-        newTabIsOpen = true;
-        gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
-        expectUncaughtException();
-        content.location = TEST_URI;
-      });
-    },
-    failureFn: finishTest,
+      newTabIsOpen = true;
+      gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
+      expectUncaughtException();
+      content.location = TEST_URI;
+    });
   });
 }
 
 function test() {
   expectUncaughtException();
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoaded, true);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js
@@ -48,27 +48,27 @@ function performTest(aRequest, aConsole)
                       "response", lastFinishedRequest.response,
                       "updates", lastFinishedRequest.updates,
                       "response headers", headers);
       }
 
       executeSoon(finishTest);
     });
 
-  HUDService.lastFinishedRequestCallback = null;
+  HUDService.lastFinishedRequest.callback = null;
 }
 
 function test()
 {
   addTab("data:text/plain;charset=utf8,hello world");
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, () => {
-      HUDService.lastFinishedRequestCallback = performTest;
+      HUDService.lastFinishedRequest.callback = performTest;
 
       browser.addEventListener("load", function onReload() {
         browser.removeEventListener("load", onReload, true);
         executeSoon(() => content.location.reload());
       }, true);
 
       executeSoon(() => content.location = TEST_URI);
     });
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
@@ -8,31 +8,31 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html";
 
 function performTest(lastFinishedRequest, aConsole)
 {
   ok(lastFinishedRequest, "charset test page was loaded and logged");
-  HUDService.lastFinishedRequestCallback = null;
+  HUDService.lastFinishedRequest.callback = null;
 
   executeSoon(() => {
     aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor,
       (aResponse) => {
         ok(!aResponse.contentDiscarded, "response body was not discarded");
 
         let body = aResponse.content.text;
         ok(body, "we have the response body");
 
         let chars = "\u7684\u95ee\u5019!"; // 的问候!
         isnot(body.indexOf("<p>" + chars + "</p>"), -1,
           "found the chinese simplified string");
 
-        HUDService.lastFinishedRequestCallback = null;
+        HUDService.lastFinishedRequest.callback = null;
         executeSoon(finishTest);
       });
   });
 }
 
 function test()
 {
   addTab("data:text/html;charset=utf-8,Web Console - bug 600183 test");
@@ -40,14 +40,14 @@ function test()
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
 
     openConsole(null, function(hud) {
       hud.ui.setSaveRequestAndResponseBodies(true).then(() => {
         ok(hud.ui._saveRequestAndResponseBodies,
           "The saveRequestAndResponseBodies property was successfully set.");
 
-        HUDService.lastFinishedRequestCallback = performTest;
+        HUDService.lastFinishedRequest.callback = performTest;
         content.location = TEST_URI;
       });
     });
   }, true);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
@@ -5,78 +5,71 @@
  *
  * Contributor(s):
  *  Mihai Șucan <mihai.sucan@gmail.com>
  *
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-601177-log-levels.html";
 
-function performTest()
-{
-  let hudId = HUDService.getHudIdByWindow(content);
-  let HUD = HUDService.hudReferences[hudId];
-
-  findEntry(HUD, "hud-networkinfo", "test-bug-601177-log-levels.html",
-            "found test-bug-601177-log-levels.html");
-
-  findEntry(HUD, "hud-networkinfo", "test-bug-601177-log-levels.js",
-            "found test-bug-601177-log-levels.js");
-
-  findEntry(HUD, "hud-networkinfo", "test-image.png", "found test-image.png");
-
-  findEntry(HUD, "hud-network", "foobar-known-to-fail.png",
-            "found foobar-known-to-fail.png");
-
-  findEntry(HUD, "hud-exception", "foobarBug601177exception",
-            "found exception");
-
-  findEntry(HUD, "hud-jswarn", "undefinedPropertyBug601177",
-            "found strict warning");
-
-  findEntry(HUD, "hud-jswarn", "foobarBug601177strictError",
-            "found strict error");
-
-  executeSoon(finishTest);
-}
-
-function findEntry(aHUD, aClass, aString, aMessage)
-{
-  return testLogEntry(aHUD.outputNode, aString, aMessage, false, false,
-                      aClass);
-}
-
 function test()
 {
   Services.prefs.setBoolPref("javascript.options.strict", true);
-
   registerCleanupFunction(function() {
     Services.prefs.clearUserPref("javascript.options.strict");
   });
 
   addTab("data:text/html;charset=utf-8,Web Console test for bug 601177: log levels");
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 
-    openConsole(null, function(hud) {
-      browser.addEventListener("load", function onLoad2() {
-        browser.removeEventListener("load", onLoad2, true);
-        waitForSuccess({
-          name: "all messages displayed",
-          validatorFn: function()
-          {
-            return hud.outputNode.itemCount >= 7;
-          },
-          successFn: performTest,
-          failureFn: function() {
-            info("itemCount: " + hud.outputNode.itemCount);
-            finishTest();
-          },
-        });
-      }, true);
+  function consoleOpened(hud)
+  {
+    expectUncaughtException();
+    content.location = TEST_URI;
+
+    info("waiting for messages");
 
-      expectUncaughtException();
-      content.location = TEST_URI;
-    });
-  }, true);
+    waitForMessages({
+      webconsole: hud,
+      messages: [
+        {
+          text: "test-bug-601177-log-levels.html",
+          category: CATEGORY_NETWORK,
+          severity: SEVERITY_LOG,
+        },
+        {
+          text: "test-bug-601177-log-levels.js",
+          category: CATEGORY_NETWORK,
+          severity: SEVERITY_LOG,
+        },
+        {
+          text: "test-image.png",
+          category: CATEGORY_NETWORK,
+          severity: SEVERITY_LOG,
+        },
+        {
+          text: "foobar-known-to-fail.png",
+          category: CATEGORY_NETWORK,
+          severity: SEVERITY_ERROR,
+        },
+        {
+          text: "foobarBug601177exception",
+          category: CATEGORY_JS,
+          severity: SEVERITY_ERROR,
+        },
+        {
+          text: "undefinedPropertyBug601177",
+          category: CATEGORY_JS,
+          severity: SEVERITY_WARNING,
+        },
+        {
+          text: "foobarBug601177strictError",
+          category: CATEGORY_JS,
+          severity: SEVERITY_WARNING,
+        },
+      ],
+    }).then(finishTest);
+  }
 }
-
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
@@ -38,18 +38,17 @@ function test()
     }));
   }, true);
 }
 
 function startTest()
 {
   // Find the relevant elements in the Web Console of tab 2.
   let win2 = tabs[runCount*2 + 1].linkedBrowser.contentWindow;
-  let hudId2 = HUDService.getHudIdByWindow(win2);
-  huds[1] = HUDService.hudReferences[hudId2];
+  huds[1] = HUDService.getHudByWindow(win2);
   info("startTest: iframe2 root height " + huds[1].ui.rootElement.clientHeight);
 
   if (runCount == 0) {
     menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodies");
   }
   else {
     menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodiesContextMenu");
   }
@@ -96,18 +95,17 @@ function testpopup2b(aEvent) {
 
     info("menupopups[1] hidden");
 
     // Switch to tab 1 and open the Web Console context menu from there.
     gBrowser.selectedTab = tabs[runCount*2];
     waitForFocus(function() {
       // Find the relevant elements in the Web Console of tab 1.
       let win1 = tabs[runCount*2].linkedBrowser.contentWindow;
-      let hudId1 = HUDService.getHudIdByWindow(win1);
-      huds[0] = HUDService.hudReferences[hudId1];
+      huds[0] = HUDService.getHudByWindow(win1);
 
       info("iframe1 root height " + huds[0].ui.rootElement.clientHeight);
 
       menuitems[0] = huds[0].ui.rootElement.querySelector("#saveBodies");
       menupopups[0] = huds[0].ui.rootElement.querySelector("menupopup");
 
       menupopups[0].addEventListener("popupshown", onpopupshown1, false);
       executeSoon(() => menupopups[0].openPopup());
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
@@ -1,70 +1,31 @@
 /* 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/. */
 
-// Tests that network log messages bring up the network panel.
+// Tests that we report JS exceptions in event handlers coming from
+// network requests, like onreadystate for XHR. See bug 618078.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-618078-network-exceptions.html";
 
-let testEnded = false;
-
-let TestObserver = {
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
-
-  observe: function test_observe(aSubject)
-  {
-    if (testEnded || !(aSubject instanceof Ci.nsIScriptError)) {
-      return;
-    }
-
-    is(aSubject.category, "content javascript", "error category");
-
-    testEnded = true;
-    if (aSubject.category == "content javascript") {
-      executeSoon(checkOutput);
-    }
-    else {
-      executeSoon(finishTest);
-    }
-  }
-};
-
-function checkOutput()
-{
-  waitForSuccess({
-    name: "exception message",
-    validatorFn: function()
-    {
-      return hud.outputNode.textContent.indexOf("bug618078exception") > -1;
-    },
-    successFn: finishTest,
-    failureFn: finishTest,
-  });
-}
-
-function testEnd()
-{
-  Services.console.unregisterListener(TestObserver);
-}
-
 function test()
 {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 618078");
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
+    openConsole(null, function(hud) {
+      expectUncaughtException();
+      content.location = TEST_URI;
 
-    openConsole(null, function(aHud) {
-      hud = aHud;
-      Services.console.registerListener(TestObserver);
-      registerCleanupFunction(testEnd);
-
-      executeSoon(function() {
-        expectUncaughtException();
-        content.location = TEST_URI;
-      });
+      waitForMessages({
+        webconsole: hud,
+        messages: [{
+          text: "bug618078exception",
+          category: CATEGORY_JS,
+          severity: SEVERITY_ERROR,
+        }],
+      }).then(finishTest);
     });
   }, true);
 }
-
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
@@ -20,38 +20,37 @@ function test() {
           category: CATEGORY_NETWORK,
           severity: SEVERITY_LOG,
         }],
       }).then(performTest);
     });
   }, true);
 }
 
-function performTest() {
-  let hudId = HUDService.getHudIdByWindow(content);
-  let HUD = HUDService.hudReferences[hudId];
+function performTest(results) {
+  let HUD = HUDService.getHudByWindow(content);
 
-  let networkMessage = HUD.outputNode.querySelector(".webconsole-msg-network");
-  ok(networkMessage, "found network message");
+  let networkMessage = [...results[0].matched][0];
+  ok(networkMessage, "network message element");
 
   let networkLink = networkMessage.querySelector(".webconsole-msg-link");
   ok(networkLink, "found network message link");
 
   let popupset = document.getElementById("mainPopupSet");
   ok(popupset, "found #mainPopupSet");
 
   let popupsShown = 0;
   let hiddenPopups = 0;
 
   let onpopupshown = function() {
     document.removeEventListener("popupshown", onpopupshown, false);
     popupsShown++;
 
     executeSoon(function() {
-      let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
+      let popups = popupset.querySelectorAll("panel[hudId=" + HUD.hudId + "]");
       is(popups.length, 1, "found one popup");
 
       document.addEventListener("popuphidden", onpopuphidden, false);
 
       registerCleanupFunction(function() {
         is(hiddenPopups, 1, "correct number of popups hidden");
         if (hiddenPopups != 1) {
           document.removeEventListener("popuphidden", onpopuphidden, false);
@@ -62,17 +61,17 @@ function performTest() {
     });
   };
 
   let onpopuphidden = function() {
     document.removeEventListener("popuphidden", onpopuphidden, false);
     hiddenPopups++;
 
     executeSoon(function() {
-      let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
+      let popups = popupset.querySelectorAll("panel[hudId=" + HUD.hudId + "]");
       is(popups.length, 0, "no popups found");
 
       executeSoon(finishTest);
     });
   };
 
   document.addEventListener("popupshown", onpopupshown, false);
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
@@ -20,17 +20,17 @@ function requestDoneCallback(aHttpReques
 
 function consoleOpened(hud)
 {
   webConsoleClient = hud.ui.webConsoleClient;
   hud.ui.setSaveRequestAndResponseBodies(true).then(() => {
     ok(hud.ui._saveRequestAndResponseBodies,
       "The saveRequestAndResponseBodies property was successfully set.");
 
-    HUDService.lastFinishedRequestCallback = requestDoneCallback;
+    HUDService.lastFinishedRequest.callback = requestDoneCallback;
     waitForSuccess(waitForResponses);
     content.location = TEST_URI;
   });
 
   let waitForResponses = {
     name: "301 and 404 responses",
     validatorFn: function()
     {
@@ -39,17 +39,17 @@ function consoleOpened(hud)
     },
     successFn: getHeaders,
     failureFn: finishTest,
   };
 }
 
 function getHeaders()
 {
-  HUDService.lastFinishedRequestCallback = null;
+  HUDService.lastFinishedRequest.callback = null;
 
   ok("301" in lastFinishedRequests, "request 1: 301 Moved Permanently");
   ok("404" in lastFinishedRequests, "request 2: 404 Not found");
 
   webConsoleClient.getResponseHeaders(lastFinishedRequests["301"].actor,
     function (aResponse) {
       lastFinishedRequests["301"].response.headers = aResponse.headers;
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -9,21 +9,18 @@ function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 }
 
 function consoleOpened(HUD) {
-  let tmp = {};
-  Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp);
-  let WCU = tmp.WebConsoleUtils;
-  let JSPropertyProvider = tmp.JSPropertyProvider;
-  tmp = null;
+  let tools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+  let JSPropertyProvider = tools.require("devtools/toolkit/webconsole/utils").JSPropertyProvider;
 
   let jsterm = HUD.jsterm;
   let win = content.wrappedJSObject;
 
   // Make sure autocomplete does not walk through iterators and generators.
   let result = win.gen1.next();
   let completion = JSPropertyProvider(win, "gen1.");
   is(completion, null, "no matches for gen1");
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js
@@ -23,17 +23,17 @@ function test()
   addTab("data:text/html;charset=utf-8,Web Console network logging tests");
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
 
     openConsole(null, function(aHud) {
       hud = aHud;
 
-      HUDService.lastFinishedRequestCallback = function(aRequest) {
+      HUDService.lastFinishedRequest.callback = function(aRequest) {
         lastRequest = aRequest;
         if (requestCallback) {
           requestCallback();
         }
       };
 
       executeSoon(testPageLoad);
     });
@@ -119,22 +119,30 @@ function testFormSubmission()
   // loaded again. Bind to the load event to catch when this is done.
   requestCallback = function() {
     ok(lastRequest, "testFormSubmission() was logged");
     is(lastRequest.request.method, "POST", "Method is correct");
 
     // There should be 3 network requests pointing to the HTML file.
     waitForMessages({
       webconsole: hud,
-      messages: [{
-        text: "test-network-request.html",
-        category: CATEGORY_NETWORK,
-        severity: SEVERITY_LOG,
-        count: 3,
-      }],
+      messages: [
+        {
+          text: "test-network-request.html",
+          category: CATEGORY_NETWORK,
+          severity: SEVERITY_LOG,
+          count: 3,
+        },
+        {
+          text: "test-data.json",
+          category: CATEGORY_NETWORK,
+          severity: SEVERITY_LOG,
+          count: 2,
+        },
+      ],
     }).then(testLiveFilteringOnSearchStrings);
   };
 
   let form = content.document.querySelector("form");
   ok(form, "we have the HTML form");
   form.submit();
 }
 
@@ -165,17 +173,17 @@ function testLiveFilteringOnSearchString
   setStringFilter("'foo'");
   is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
     "the string 'foo'");
 
   setStringFilter("foo\"bar'baz\"boo'");
   is(countMessageNodes(), 0, "the log nodes are hidden when searching for " +
     "the string \"foo\"bar'baz\"boo'\"");
 
-  HUDService.lastFinishedRequestCallback = null;
+  HUDService.lastFinishedRequest.callback = null;
   lastRequest = null;
   requestCallback = null;
   finishTest();
 }
 
 function countMessageNodes() {
   let messageNodes = hud.outputNode.querySelectorAll(".hud-msg-node");
   let displayedMessageNodes = 0;
--- 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
@@ -17,21 +17,16 @@ function test() {
 let gHUD;
 
 function consoleOpened(aHud) {
   gHUD = aHud;
   let jsterm = gHUD.jsterm;
   let popup = jsterm.autocompletePopup;
   let completeNode = jsterm.completeNode;
 
-  let tmp = {};
-  Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp);
-  let WCU = tmp.WebConsoleUtils;
-  tmp = null;
-
   ok(!popup.isOpen, "popup is not open");
 
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
     // expected properties:
--- 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
@@ -79,56 +79,39 @@ function performTestComparisons()
 
 function performWebConsoleTests(hud)
 {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let jsterm = hud.jsterm;
   outputNode = hud.outputNode;
 
   jsterm.clearOutput();
-  jsterm.execute("$0");
+  jsterm.execute("$0", onNodeOutput);
 
-  waitForSuccess({
-    name: "$0 output",
-    validatorFn: function()
-    {
-      return outputNode.querySelector(".webconsole-msg-output");
-    },
-    successFn: function()
-    {
-      let node = outputNode.querySelector(".webconsole-msg-output");
-      isnot(node.textContent.indexOf("[object HTMLHeadingElement"), -1,
-            "correct output for $0");
+  function onNodeOutput()
+  {
+    let node = outputNode.querySelector(".webconsole-msg-output");
+    isnot(node.textContent.indexOf("[object HTMLHeadingElement"), -1,
+          "correct output for $0");
+
+    jsterm.clearOutput();
+    jsterm.execute("$0.textContent = 'bug653531'", onNodeUpdate);
+  }
 
-      jsterm.clearOutput();
-      jsterm.execute("$0.textContent = 'bug653531'");
-      waitForSuccess(waitForNodeUpdate);
-    },
-    failureFn: finishUp,
-  });
+  function onNodeUpdate()
+  {
+    let node = outputNode.querySelector(".webconsole-msg-output");
+    isnot(node.textContent.indexOf("bug653531"), -1,
+          "correct output for $0.textContent");
+    let inspector = gDevTools.getToolbox(target).getPanel("inspector");
+    is(inspector.selection.node.textContent, "bug653531",
+       "node successfully updated");
 
-  let waitForNodeUpdate = {
-    name: "$0.textContent update",
-    validatorFn: function()
-    {
-      return outputNode.querySelector(".webconsole-msg-output");
-    },
-    successFn: function()
-    {
-      let node = outputNode.querySelector(".webconsole-msg-output");
-      isnot(node.textContent.indexOf("bug653531"), -1,
-            "correct output for $0.textContent");
-      let inspector = gDevTools.getToolbox(target).getPanel("inspector");
-      is(inspector.selection.node.textContent, "bug653531",
-         "node successfully updated");
-
-      executeSoon(finishUp);
-    },
-    failureFn: finishUp,
-  };
+    executeSoon(finishUp);
+  }
 }
 
 function finishUp() {
   finishTest();
 }
 
 function test()
 {
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
@@ -54,18 +54,17 @@ function testTimerIndependenceInTabs(hud
       executeSoon(testTimerIndependenceInSameTab);
     }, true);
     content.location = "data:text/html;charset=utf-8,<script type='text/javascript'>" +
            "console.time('bTimer');</script>";
   });
 }
 
 function testTimerIndependenceInSameTab() {
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+  let hud = HUDService.getHudByWindow(content);
   outputNode = hud.outputNode;
 
   waitForSuccess({
     name: "bTimer started",
     validatorFn: function()
     {
       return outputNode.textContent.indexOf("bTimer: timer started") > -1;
     },
@@ -82,18 +81,17 @@ function testTimerIndependenceInSameTab(
         "<script type='text/javascript'>" +
         "console.timeEnd('bTimer');</script>";
     },
     failureFn: finishTest,
   });
 }
 
 function testTimerIndependenceInSameTabAgain() {
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+  let hud = HUDService.getHudByWindow(content);
   outputNode = hud.outputNode;
 
   executeSoon(function() {
     testLogEntry(outputNode, "bTimer: timer started", "bTimer was not started",
                  false, true);
 
     closeConsole(gBrowser.selectedTab, function() {
       gBrowser.removeCurrentTab();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/*
+ * Tests that errors about insecure passwords are logged
+ * to the web console
+ */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-about-blank-web-console-warning.html";
+const INSECURE_PASSWORD_MSG = "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen.";
+
+function test() {
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, onLoad, true);
+    openConsole(null, function testInsecurePasswordErrorLogged (hud) {
+      waitForMessages({
+        webconsole: hud,
+        messages: [
+          {
+            name: "Insecure password error displayed successfully",
+            text: INSECURE_PASSWORD_MSG,
+            category: CATEGORY_SECURITY,
+            severity: SEVERITY_WARNING
+          },
+        ],
+      }).then(finishTest);
+    });
+  }, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/*
+ * Tests that errors about insecure passwords are logged
+ * to the web console
+ */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-762593-insecure-passwords-web-console-warning.html";
+const INSECURE_PASSWORD_MSG = "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen.";
+const INSECURE_FORM_ACTION_MSG = "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen.";
+const INSECURE_IFRAME_MSG = "Password fields present on an insecure (http://) iframe. This is a security risk that allows user login credentials to be stolen.";
+const INSECURE_PASSWORDS_URI = "https://developer.mozilla.org/en-US/docs/Security/InsecurePasswords";
+
+function test() {
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, onLoad, true);
+    openConsole(null, function testInsecurePasswordErrorLogged (hud) {
+      waitForMessages({
+        webconsole: hud,
+        messages: [
+          {
+            name: "Insecure password error displayed successfully",
+            text: INSECURE_PASSWORD_MSG,
+            category: CATEGORY_SECURITY,
+            severity: SEVERITY_WARNING
+          },
+          {
+            name: "Insecure iframe error displayed successfully",
+            text: INSECURE_IFRAME_MSG,
+            category: CATEGORY_SECURITY,
+            severity: SEVERITY_WARNING
+          },
+          {
+            name: "Insecure form action error displayed successfully",
+            text: INSECURE_FORM_ACTION_MSG,
+            category: CATEGORY_SECURITY,
+            severity: SEVERITY_WARNING
+          },
+        ],
+      }).then( () => testClickOpenNewTab(hud));
+    });
+  }, true);
+}
+
+function testClickOpenNewTab(hud) {
+  let warningNode = hud.outputNode.querySelector(
+    ".webconsole-msg-body .webconsole-learn-more-link");
+
+  /*
+   * Invoke the click event and check if a new tab would open to the correct
+   * page
+   */
+  let linkOpened = false;
+  let oldOpenUILinkIn = window.openUILinkIn;
+  window.openUILinkIn = function(aLink) {
+    if (aLink == INSECURE_PASSWORDS_URI) {
+      linkOpened = true;
+    }
+  }
+
+  EventUtils.synthesizeMouse(warningNode, 2, 2, {},
+                             warningNode.ownerDocument.defaultView);
+  ok(linkOpened, "Clicking the Insecure Passwords Warning node opens the desired page");
+  window.openUILinkIn = oldOpenUILinkIn;
+
+  finishTest();
+}
--- a/browser/devtools/webconsole/test/browser_we