merge m-c into fig
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Sun, 28 Jul 2013 12:08:01 -0700
changeset 143466 c183395271416d3b96ebcded68fac6c31f9dc3a8
parent 143465 008632d5460705de6ae26aa47db503e732758735 (current diff)
parent 140267 73b69c146ca6926b4a72bb484550e5afe04b93cc (diff)
child 143467 ba63590416075b1c196c15fc27bcdcad358ef1c5
push id25130
push userlrocha@mozilla.com
push dateWed, 21 Aug 2013 09:41:27 +0000
treeherdermozilla-central@b2486721572e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.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 into fig
CLOBBER
accessible/tests/mochitest/text/test_multiline.html
accessible/tests/mochitest/text/test_singleline.html
browser/devtools/webconsole/test/test-bug-595934-canvas.html
browser/devtools/webconsole/test/test-bug-595934-canvas.js
browser/metro/theme/images/appbar-back.png
browser/metro/theme/images/appbar-back@1.4x.png
browser/metro/theme/images/appbar-forward.png
browser/metro/theme/images/appbar-forward@1.4x.png
browser/metro/theme/images/appbar-menu.png
browser/metro/theme/images/appbar-menu@1.4x.png
browser/metro/theme/images/appbar-pin.png
browser/metro/theme/images/appbar-pin@1.4x.png
browser/metro/theme/images/appbar-reload.png
browser/metro/theme/images/appbar-star.png
browser/metro/theme/images/appbar-star@1.4x.png
browser/metro/theme/images/appbar-stop.png
dom/mobilemessage/interfaces/nsIDOMSmsManager.idl
dom/mobilemessage/src/SmsManager.cpp
dom/mobilemessage/src/SmsManager.h
dom/plugins/test/mochitest/test_instance_re-parent-windowed.html
gfx/layers/GonkIOSurfaceImage.cpp
gfx/layers/GonkIOSurfaceImage.h
image/decoders/nsWBMPDecoder.cpp
image/decoders/nsWBMPDecoder.h
image/test/reftest/wbmp/reftest.list
image/test/reftest/wbmp/wbmp-corrupted/err_fh_b0.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_fh_b1.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_fh_b2.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_fh_b3.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_fh_b4.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_fh_b7_and_ext_header.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_incomplete_height.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_incomplete_width.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_type_1.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_type_fix_only.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_type_fix_width_only.wbmp
image/test/reftest/wbmp/wbmp-corrupted/err_type_header_only.wbmp
image/test/reftest/wbmp/wbmp-corrupted/height_0.wbmp
image/test/reftest/wbmp/wbmp-corrupted/over_height.wbmp
image/test/reftest/wbmp/wbmp-corrupted/over_width.wbmp
image/test/reftest/wbmp/wbmp-corrupted/reftest.list
image/test/reftest/wbmp/wbmp-corrupted/width_0.wbmp
image/test/reftest/wbmp/wbmp-corrupted/width_0_height_0.wbmp
image/test/reftest/wbmp/wbmp-corrupted/wrapper.html
image/test/reftest/wbmp/wbmp-normal/bmts.png
image/test/reftest/wbmp/wbmp-normal/bmts.wbmp
image/test/reftest/wbmp/wbmp-normal/height_1.png
image/test/reftest/wbmp/wbmp-normal/height_1.wbmp
image/test/reftest/wbmp/wbmp-normal/large.png
image/test/reftest/wbmp/wbmp-normal/large.wbmp
image/test/reftest/wbmp/wbmp-normal/multi_byte_height.png
image/test/reftest/wbmp/wbmp-normal/multi_byte_height.wbmp
image/test/reftest/wbmp/wbmp-normal/multi_byte_width.png
image/test/reftest/wbmp/wbmp-normal/multi_byte_width.wbmp
image/test/reftest/wbmp/wbmp-normal/ok_fh_b5.png
image/test/reftest/wbmp/wbmp-normal/ok_fh_b5.wbmp
image/test/reftest/wbmp/wbmp-normal/ok_fh_b6.png
image/test/reftest/wbmp/wbmp-normal/ok_fh_b6.wbmp
image/test/reftest/wbmp/wbmp-normal/reftest.list
image/test/reftest/wbmp/wbmp-normal/width_1.png
image/test/reftest/wbmp/wbmp-normal/width_1.wbmp
image/test/reftest/wbmp/wbmp-normal/width_1_height_1.png
image/test/reftest/wbmp/wbmp-normal/width_1_height_1.wbmp
js/src/jit-test/tests/collections/Array-iterator-surfaces.js
js/src/jit-test/tests/parallelarray/alloc-array-nonwritable.js
js/src/jit-test/tests/parallelarray/alloc-array.js
js/src/jit-test/tests/parallelarray/alloc-different-objs.js
js/src/jit-test/tests/parallelarray/alloc-many-objs.js
js/src/jit-test/tests/parallelarray/alloc-obj.js
js/src/jit-test/tests/parallelarray/alloc-too-many-objs.js
js/src/jit-test/tests/parallelarray/bailout-executed.js
js/src/jit-test/tests/parallelarray/bailout-never-executed.js
js/src/jit-test/tests/parallelarray/binary-arith-numbers.js
js/src/jit-test/tests/parallelarray/bug783924.js
js/src/jit-test/tests/parallelarray/bug787282.js
js/src/jit-test/tests/parallelarray/bug853555.js
js/src/jit-test/tests/parallelarray/bug853573.js
js/src/jit-test/tests/parallelarray/bug853576.js
js/src/jit-test/tests/parallelarray/bug854021.js
js/src/jit-test/tests/parallelarray/bug854050.js
js/src/jit-test/tests/parallelarray/bug854381.js
js/src/jit-test/tests/parallelarray/bug857846.js
js/src/jit-test/tests/parallelarray/bug858077.js
js/src/jit-test/tests/parallelarray/bug858582.js
js/src/jit-test/tests/parallelarray/bug890465.js
js/src/jit-test/tests/parallelarray/bug894782.js
js/src/jit-test/tests/parallelarray/closure-1.js
js/src/jit-test/tests/parallelarray/closure-2.js
js/src/jit-test/tests/parallelarray/closure-3.js
js/src/jit-test/tests/parallelarray/closure-4.js
js/src/jit-test/tests/parallelarray/closure-5.js
js/src/jit-test/tests/parallelarray/closure-6.js
js/src/jit-test/tests/parallelarray/closure-7.js
js/src/jit-test/tests/parallelarray/closure-8.js
js/src/jit-test/tests/parallelarray/compare-strings-eq.js
js/src/jit-test/tests/parallelarray/compare-strings-ne.js
js/src/jit-test/tests/parallelarray/compare-values.js
js/src/jit-test/tests/parallelarray/comprehension-1.js
js/src/jit-test/tests/parallelarray/comprehension-2.js
js/src/jit-test/tests/parallelarray/comprehension-fn-args.js
js/src/jit-test/tests/parallelarray/comprehension-in-loop.js
js/src/jit-test/tests/parallelarray/comprehension-nested.js
js/src/jit-test/tests/parallelarray/comprehension-scale.js
js/src/jit-test/tests/parallelarray/comprehension-throws.js
js/src/jit-test/tests/parallelarray/constructor-1.js
js/src/jit-test/tests/parallelarray/constructor-2.js
js/src/jit-test/tests/parallelarray/constructor-3.js
js/src/jit-test/tests/parallelarray/constructor-4.js
js/src/jit-test/tests/parallelarray/constructor-5.js
js/src/jit-test/tests/parallelarray/constructor-throws.js
js/src/jit-test/tests/parallelarray/element-1.js
js/src/jit-test/tests/parallelarray/element-2.js
js/src/jit-test/tests/parallelarray/element-3.js
js/src/jit-test/tests/parallelarray/enumerate-1.js
js/src/jit-test/tests/parallelarray/filter-all.js
js/src/jit-test/tests/parallelarray/filter-every-third-element.js
js/src/jit-test/tests/parallelarray/filter-non-divisible.js
js/src/jit-test/tests/parallelarray/filter-none.js
js/src/jit-test/tests/parallelarray/filter-truthy.js
js/src/jit-test/tests/parallelarray/filter-very-few.js
js/src/jit-test/tests/parallelarray/flatten-1.js
js/src/jit-test/tests/parallelarray/flatten-2.js
js/src/jit-test/tests/parallelarray/flatten-3.js
js/src/jit-test/tests/parallelarray/flatten-throws.js
js/src/jit-test/tests/parallelarray/get-1.js
js/src/jit-test/tests/parallelarray/get-2.js
js/src/jit-test/tests/parallelarray/get-3.js
js/src/jit-test/tests/parallelarray/get-4.js
js/src/jit-test/tests/parallelarray/get-6.js
js/src/jit-test/tests/parallelarray/holes-1.js
js/src/jit-test/tests/parallelarray/holes-2.js
js/src/jit-test/tests/parallelarray/index-1.js
js/src/jit-test/tests/parallelarray/index-2.js
js/src/jit-test/tests/parallelarray/index-3.js
js/src/jit-test/tests/parallelarray/index-4.js
js/src/jit-test/tests/parallelarray/inline-new-bad-type.js
js/src/jit-test/tests/parallelarray/length-1.js
js/src/jit-test/tests/parallelarray/length-2.js
js/src/jit-test/tests/parallelarray/length-3.js
js/src/jit-test/tests/parallelarray/mandelbrot.js
js/src/jit-test/tests/parallelarray/map-2.js
js/src/jit-test/tests/parallelarray/map-3.js
js/src/jit-test/tests/parallelarray/map-add-from-upvar-field.js
js/src/jit-test/tests/parallelarray/map-factorial.js
js/src/jit-test/tests/parallelarray/map-fn-args.js
js/src/jit-test/tests/parallelarray/map-inc.js
js/src/jit-test/tests/parallelarray/map-nested.js
js/src/jit-test/tests/parallelarray/map-parallel-assign-to-def-prop.js
js/src/jit-test/tests/parallelarray/map-short.js
js/src/jit-test/tests/parallelarray/map-throws.js
js/src/jit-test/tests/parallelarray/nested-sum-each-row.js
js/src/jit-test/tests/parallelarray/overflow-throws.js
js/src/jit-test/tests/parallelarray/parallel-getelement-ic.js
js/src/jit-test/tests/parallelarray/parallel-getproperty-ic.js
js/src/jit-test/tests/parallelarray/parallelarraycompiledout.js
js/src/jit-test/tests/parallelarray/partition-1.js
js/src/jit-test/tests/parallelarray/partition-throws.js
js/src/jit-test/tests/parallelarray/reduce-bail.js
js/src/jit-test/tests/parallelarray/reduce-fn-args.js
js/src/jit-test/tests/parallelarray/reduce-higher-dim.js
js/src/jit-test/tests/parallelarray/reduce-length-one.js
js/src/jit-test/tests/parallelarray/reduce-mul-short.js
js/src/jit-test/tests/parallelarray/reduce-mul.js
js/src/jit-test/tests/parallelarray/reduce-sum.js
js/src/jit-test/tests/parallelarray/reduce-throws.js
js/src/jit-test/tests/parallelarray/scan-1.js
js/src/jit-test/tests/parallelarray/scan-2.js
js/src/jit-test/tests/parallelarray/scan-3.js
js/src/jit-test/tests/parallelarray/scan-fn-args.js
js/src/jit-test/tests/parallelarray/scan-sorted.js
js/src/jit-test/tests/parallelarray/scan-throws.js
js/src/jit-test/tests/parallelarray/scatter-1.js
js/src/jit-test/tests/parallelarray/scatter-10.js
js/src/jit-test/tests/parallelarray/scatter-11.js
js/src/jit-test/tests/parallelarray/scatter-12.js
js/src/jit-test/tests/parallelarray/scatter-13.js
js/src/jit-test/tests/parallelarray/scatter-2.js
js/src/jit-test/tests/parallelarray/scatter-3.js
js/src/jit-test/tests/parallelarray/scatter-4.js
js/src/jit-test/tests/parallelarray/scatter-5.js
js/src/jit-test/tests/parallelarray/scatter-6.js
js/src/jit-test/tests/parallelarray/scatter-7.js
js/src/jit-test/tests/parallelarray/scatter-8.js
js/src/jit-test/tests/parallelarray/scatter-9.js
js/src/jit-test/tests/parallelarray/scatter-regression-1.js
js/src/jit-test/tests/parallelarray/scatter-throws.js
js/src/jit-test/tests/parallelarray/shape-1.js
js/src/jit-test/tests/parallelarray/shape-2.js
js/src/jit-test/tests/parallelarray/shape-3.js
js/src/jit-test/tests/parallelarray/shape-4.js
js/src/jit-test/tests/parallelarray/shape-5.js
js/src/jit-test/tests/parallelarray/spew.js
js/src/jit-test/tests/parallelarray/stack-overflow.js
js/src/jit-test/tests/parallelarray/strict-equals-1.js
js/src/jit-test/tests/parallelarray/strings.js
js/src/jit-test/tests/parallelarray/surfaces-1.js
js/src/jit-test/tests/parallelarray/surfaces-2.js
js/src/jit-test/tests/parallelarray/surfaces-3.js
js/src/jit-test/tests/parallelarray/throw-executed.js
js/src/jit-test/tests/parallelarray/throw-never-executed.js
js/src/jit-test/tests/parallelarray/timeout-gc.js
js/src/jit-test/tests/parallelarray/timeout.js
js/src/jit-test/tests/parallelarray/toString-1.js
js/src/jit-test/tests/parallelarray/write-array.js
js/src/jit-test/tests/parallelarray/write-illegal-array-elt.js
js/src/jit-test/tests/parallelarray/write-illegal-obj.js
js/src/jit-test/tests/parallelarray/write-obj.js
layout/style/test/redundant_font_download.sjs
layout/style/test/test_redundant_font_download.html
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoApp.java
mobile/android/base/Makefile.in
mobile/android/base/ProfileMigrator.java
mobile/android/base/TabsTray.java
mobile/android/base/db/LocalBrowserDB.java
mobile/android/base/home/BookmarksPage.java
mobile/android/base/home/FaviconsLoader.java
mobile/android/base/home/TwoLinePageRow.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/strings.xml.in
mobile/android/base/sync/repositories/android/FennecControlHelper.java
mobile/android/base/tests/BaseTest.java.in
mobile/android/base/tests/assets/places.sqlite.zip
mobile/android/base/tests/robocop.ini
mobile/android/base/tests/robocop_autophone.ini
mobile/android/base/tests/testBookmark.java.in
mobile/android/base/tests/testMigration.java.in
mobile/android/chrome/content/browser.js
testing/mozbase/manifestdestiny/README.md
testing/mozbase/mozfile/tests/test.py
testing/mozbase/mozhttpd/README.md
testing/mozbase/mozhttpd/mozhttpd/iface.py
testing/mozbase/mozlog/README.md
testing/mozbase/moztest/README.md
toolkit/components/search/tests/xpcshell/test_migratedb.js
toolkit/themes/linux/mozapps/plugins/pluginDisabled-16.png
toolkit/themes/linux/mozapps/plugins/pluginDisabled.png
toolkit/themes/osx/mozapps/plugins/contentPluginBlocked.png
toolkit/themes/osx/mozapps/plugins/contentPluginClickToPlay.png
toolkit/themes/osx/mozapps/plugins/contentPluginClickToPlayPlain.png
toolkit/themes/osx/mozapps/plugins/contentPluginClose.png
toolkit/themes/osx/mozapps/plugins/contentPluginCrashed.png
toolkit/themes/osx/mozapps/plugins/contentPluginDisabled.png
toolkit/themes/osx/mozapps/plugins/contentPluginDownload.png
toolkit/themes/osx/mozapps/plugins/contentPluginMissing.png
toolkit/themes/osx/mozapps/plugins/pluginDisabled-16.png
toolkit/themes/osx/mozapps/plugins/pluginDisabled.png
toolkit/themes/osx/mozapps/plugins/pluginProblem.css
toolkit/themes/windows/mozapps/plugins/contentPluginBlocked.png
toolkit/themes/windows/mozapps/plugins/contentPluginClickToPlay.png
toolkit/themes/windows/mozapps/plugins/contentPluginClickToPlayPlain.png
toolkit/themes/windows/mozapps/plugins/contentPluginClose.png
toolkit/themes/windows/mozapps/plugins/contentPluginCrashed.png
toolkit/themes/windows/mozapps/plugins/contentPluginDisabled.png
toolkit/themes/windows/mozapps/plugins/contentPluginDownload.png
toolkit/themes/windows/mozapps/plugins/contentPluginMissing.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled-16-aero.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled-16.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled-aero.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled.png
toolkit/themes/windows/mozapps/plugins/pluginProblem.css
xpcom/base/nsAtomicRefcnt.h
xpcom/glue/SSE.cpp
xpcom/glue/SSE.h
xpcom/glue/arm.cpp
xpcom/glue/arm.h
xpcom/tests/ShowSSEConfig.cpp
--- a/accessible/src/base/ARIAMap.cpp
+++ b/accessible/src/base/ARIAMap.cpp
@@ -171,19 +171,20 @@ static nsRoleMapEntry sWAIRoleMaps[] =
   { // grid
     &nsGkAtoms::grid,
     roles::TABLE,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
     eSelect | eTable,
-    states::FOCUSABLE,
+    kNoReqStates,
     eARIAMultiSelectable,
-    eARIAReadonlyOrEditable
+    eARIAReadonlyOrEditable,
+    eFocusableUntilDisabled
   },
   { // gridcell
     &nsGkAtoms::gridcell,
     roles::GRID_CELL,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
@@ -258,17 +259,18 @@ static nsRoleMapEntry sWAIRoleMaps[] =
     roles::LISTBOX,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
     eListControl | eSelect,
     kNoReqStates,
     eARIAMultiSelectable,
-    eARIAReadonly
+    eARIAReadonly,
+    eFocusableUntilDisabled
   },
   { // listitem
     &nsGkAtoms::listitem,
     roles::LISTITEM,
     kUseMapRole,
     eNoValue,
     eNoAction, // XXX: should depend on state, parent accessible
     eNoLiveAttr,
@@ -534,17 +536,17 @@ static nsRoleMapEntry sWAIRoleMaps[] =
     eARIASelectable
   },
   { // tablist
     &nsGkAtoms::tablist,
     roles::PAGETABLIST,
     kUseMapRole,
     eNoValue,
     eNoAction,
-    ePoliteLiveAttr,
+    eNoLiveAttr,
     eSelect,
     kNoReqStates
   },
   { // tabpanel
     &nsGkAtoms::tabpanel,
     roles::PROPERTYPAGE,
     kUseMapRole,
     eNoValue,
@@ -600,29 +602,31 @@ static nsRoleMapEntry sWAIRoleMaps[] =
     roles::OUTLINE,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
     eSelect,
     kNoReqStates,
     eARIAReadonly,
-    eARIAMultiSelectable
+    eARIAMultiSelectable,
+    eFocusableUntilDisabled
   },
   { // treegrid
     &nsGkAtoms::treegrid,
     roles::TREE_TABLE,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
     eSelect | eTable,
     kNoReqStates,
     eARIAReadonly,
-    eARIAMultiSelectable
+    eARIAMultiSelectable,
+    eFocusableUntilDisabled
   },
   { // treeitem
     &nsGkAtoms::treeitem,
     roles::OUTLINEITEM,
     kUseMapRole,
     eNoValue,
     eActivateAction, // XXX: should expose second 'expand/collapse' action based
                      // on states
--- a/accessible/src/base/ARIAStateMap.cpp
+++ b/accessible/src/base/ARIAStateMap.cpp
@@ -1,15 +1,16 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "ARIAMap.h"
+#include "nsAccUtils.h"
 #include "States.h"
 
 #include "mozilla/dom/Element.h"
 
 using namespace mozilla;
 using namespace mozilla::a11y;
 using namespace mozilla::a11y::aria;
 
@@ -322,16 +323,26 @@ aria::MapToState(EStateRule aRule, dom::
     {
       if (!aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow) &&
           !aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext))
         *aState |= states::MIXED;
 
       return true;
     }
 
+    case eFocusableUntilDisabled:
+    {
+      if (!nsAccUtils::HasDefinedARIAToken(aElement, nsGkAtoms::aria_disabled) ||
+          aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_disabled,
+                                nsGkAtoms::_false, eCaseMatters))
+        *aState |= states::FOCUSABLE;
+
+      return true;
+    }
+
     default:
       return false;
   }
 }
 
 static void
 MapEnumType(dom::Element* aElement, uint64_t* aState, const EnumTypeData& aData)
 {
--- a/accessible/src/base/ARIAStateMap.h
+++ b/accessible/src/base/ARIAStateMap.h
@@ -39,17 +39,18 @@ enum EStateRule
   eARIAPressed,
   eARIAReadonly,
   eARIAReadonlyOrEditable,
   eARIAReadonlyOrEditableIfDefined,
   eARIARequired,
   eARIASelectable,
   eARIASelectableIfDefined,
   eReadonlyUntilEditable,
-  eIndeterminateIfNoValue
+  eIndeterminateIfNoValue,
+  eFocusableUntilDisabled
 };
 
 /**
  * Expose the accessible states for the given element accordingly to state
  * mapping rule.
  *
  * @param  aRule     [in] state mapping rule ID
  * @param  aElement  [in] node of the accessible
--- a/accessible/src/base/DocManager.cpp
+++ b/accessible/src/base/DocManager.cpp
@@ -112,20 +112,20 @@ DocManager::Shutdown()
     progress->RemoveProgressListener(static_cast<nsIWebProgressListener*>(this));
 
   ClearDocCache();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsISupports
 
-NS_IMPL_THREADSAFE_ISUPPORTS3(DocManager,
-                              nsIWebProgressListener,
-                              nsIDOMEventListener,
-                              nsISupportsWeakReference)
+NS_IMPL_ISUPPORTS3(DocManager,
+                   nsIWebProgressListener,
+                   nsIDOMEventListener,
+                   nsISupportsWeakReference)
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsIWebProgressListener
 
 NS_IMETHODIMP
 DocManager::OnStateChange(nsIWebProgress* aWebProgress,
                           nsIRequest* aRequest, uint32_t aStateFlags,
                           nsresult aStatus)
--- a/accessible/src/base/DocManager.h
+++ b/accessible/src/base/DocManager.h
@@ -24,17 +24,17 @@ class DocAccessible;
  */
 class DocManager : public nsIWebProgressListener,
                    public nsIDOMEventListener,
                    public nsSupportsWeakReference
 {
 public:
   virtual ~DocManager() { }
 
-  NS_DECL_ISUPPORTS
+  NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIWEBPROGRESSLISTENER
   NS_DECL_NSIDOMEVENTLISTENER
 
   /**
    * Return document accessible for the given DOM node.
    */
   DocAccessible* GetDocAccessible(nsIDocument* aDocument);
 
--- a/accessible/src/base/nsAccessiblePivot.cpp
+++ b/accessible/src/base/nsAccessiblePivot.cpp
@@ -293,36 +293,119 @@ nsAccessiblePivot::MoveLast(nsIAccessibl
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (accessible)
     *aResult = MovePivotInternal(accessible, nsAccessiblePivot::REASON_LAST);
 
   return NS_OK;
 }
 
-// TODO: Implement
 NS_IMETHODIMP
 nsAccessiblePivot::MoveNextByText(TextBoundaryType aBoundary, bool* aResult)
 {
   NS_ENSURE_ARG(aResult);
 
   *aResult = false;
 
-  return NS_ERROR_NOT_IMPLEMENTED;
+  int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
+  HyperTextAccessible* text = mPosition->AsHyperText();
+  Accessible* oldPosition = mPosition;
+  while (!text) {
+    oldPosition = mPosition;
+    mPosition = mPosition->Parent();
+    text = mPosition->AsHyperText();
+  }
+
+  if (mEndOffset == -1)
+    mEndOffset = text != oldPosition ? text->GetChildOffset(oldPosition) : 0;
+
+  if (mEndOffset == text->CharacterCount())
+    return NS_OK;
+
+  AccessibleTextBoundary startBoundary, endBoundary;
+  switch (aBoundary) {
+    case CHAR_BOUNDARY:
+      startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
+      endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
+      break;
+    case WORD_BOUNDARY:
+      startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
+      endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
+      break;
+    default:
+      return NS_ERROR_INVALID_ARG;
+  }
+
+  nsAutoString unusedText;
+  int32_t newStart = 0, newEnd = 0;
+  text->GetTextAtOffset(mEndOffset, endBoundary, &newStart, &mEndOffset, unusedText);
+  text->GetTextBeforeOffset(mEndOffset, startBoundary, &newStart, &newEnd,
+                            unusedText);
+  mStartOffset = newEnd == mEndOffset ? newStart : newEnd;
+
+  *aResult = true;
+
+  NotifyOfPivotChange(mPosition, oldStart, oldEnd,
+                      nsIAccessiblePivot::REASON_TEXT);
+  return NS_OK;
 }
 
-// TODO: Implement
 NS_IMETHODIMP
 nsAccessiblePivot::MovePreviousByText(TextBoundaryType aBoundary, bool* aResult)
 {
   NS_ENSURE_ARG(aResult);
 
   *aResult = false;
 
-  return NS_ERROR_NOT_IMPLEMENTED;
+  int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
+  HyperTextAccessible* text = mPosition->AsHyperText();
+  Accessible* oldPosition = mPosition;
+  while (!text) {
+    oldPosition = mPosition;
+    mPosition = mPosition->Parent();
+    text = mPosition->AsHyperText();
+  }
+
+  if (mStartOffset == -1)
+    mStartOffset = text != oldPosition ? text->GetChildOffset(oldPosition) : 0;
+
+  if (mStartOffset == 0)
+    return NS_OK;
+
+  AccessibleTextBoundary startBoundary, endBoundary;
+  switch (aBoundary) {
+    case CHAR_BOUNDARY:
+      startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
+      endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
+      break;
+    case WORD_BOUNDARY:
+      startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
+      endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
+      break;
+    default:
+      return NS_ERROR_INVALID_ARG;
+  }
+
+  nsAutoString unusedText;
+  int32_t newStart = 0, newEnd = 0;
+  text->GetTextBeforeOffset(mStartOffset, startBoundary, &newStart, &newEnd,
+                            unusedText);
+  if (newStart < mStartOffset)
+    mStartOffset = newEnd == mStartOffset ? newStart : newEnd;
+  else // XXX: In certain odd cases newStart is equal to mStartOffset
+    text->GetTextBeforeOffset(mStartOffset - 1, startBoundary, &newStart,
+                              &mStartOffset, unusedText);
+  text->GetTextAtOffset(mStartOffset, endBoundary, &newStart, &mEndOffset,
+                        unusedText);
+
+  *aResult = true;
+
+  NotifyOfPivotChange(mPosition, oldStart, oldEnd,
+                      nsIAccessiblePivot::REASON_TEXT);
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAccessiblePivot::MoveToPoint(nsIAccessibleTraversalRule* aRule,
                                int32_t aX, int32_t aY, bool aIgnoreNoMatch,
                                bool* aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
--- a/accessible/src/base/nsCoreUtils.cpp
+++ b/accessible/src/base/nsCoreUtils.cpp
@@ -27,16 +27,17 @@
 #include "nsEventStateManager.h"
 #include "nsISelectionPrivate.h"
 #include "nsISelectionController.h"
 #include "nsPIDOMWindow.h"
 #include "nsGUIEvent.h"
 #include "nsView.h"
 #include "nsLayoutUtils.h"
 #include "nsGkAtoms.h"
+#include "nsDOMTouchEvent.h"
 
 #include "nsComponentManagerUtils.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "mozilla/dom/Element.h"
 
 #include "nsITreeBoxObject.h"
 #include "nsITreeColumns.h"
 
@@ -108,50 +109,24 @@ nsCoreUtils::DispatchClickEvent(nsITreeB
 
   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,
                      tcContent, tcFrame, presShell, rootWidget);
 
   DispatchMouseEvent(NS_MOUSE_BUTTON_UP, cnvdX, cnvdY,
                      tcContent, tcFrame, presShell, rootWidget);
 }
 
-bool
-nsCoreUtils::DispatchMouseEvent(uint32_t aEventType,
-                                nsIPresShell *aPresShell,
-                                nsIContent *aContent)
-{
-  nsIFrame *frame = aContent->GetPrimaryFrame();
-  if (!frame)
-    return false;
-
-  // Compute x and y coordinates.
-  nsPoint point;
-  nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point);
-  if (!widget)
-    return false;
-
-  nsSize size = frame->GetSize();
-
-  nsPresContext* presContext = aPresShell->GetPresContext();
-
-  int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2);
-  int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2);
-
-  // Fire mouse event.
-  DispatchMouseEvent(aEventType, x, y, aContent, frame, aPresShell, widget);
-  return true;
-}
-
 void
 nsCoreUtils::DispatchMouseEvent(uint32_t aEventType, int32_t aX, int32_t aY,
                                 nsIContent *aContent, nsIFrame *aFrame,
                                 nsIPresShell *aPresShell, nsIWidget *aRootWidget)
 {
   nsMouseEvent event(true, aEventType, aRootWidget,
                      nsMouseEvent::eReal, nsMouseEvent::eNormal);
 
@@ -161,16 +136,38 @@ nsCoreUtils::DispatchMouseEvent(uint32_t
   event.button = nsMouseEvent::eLeftButton;
   event.time = PR_IntervalNow();
   event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN;
 
   nsEventStatus status = nsEventStatus_eIgnore;
   aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status);
 }
 
+void
+nsCoreUtils::DispatchTouchEvent(uint32_t aEventType, int32_t aX, int32_t aY,
+                                nsIContent* aContent, nsIFrame* aFrame,
+                                nsIPresShell* aPresShell, nsIWidget* aRootWidget)
+{
+  if (!nsDOMTouchEvent::PrefEnabled())
+    return;
+
+  nsTouchEvent event(true, aEventType, aRootWidget);
+
+  event.time = PR_IntervalNow();
+
+  // XXX: Touch has an identifier of -1 to hint that it is synthesized.
+  nsRefPtr<mozilla::dom::Touch> t =
+    new mozilla::dom::Touch(-1, nsIntPoint(aX, aY),
+                            nsIntPoint(1, 1), 0.0f, 1.0f);
+  t->SetTarget(aContent);
+  event.touches.AppendElement(t);
+  nsEventStatus status = nsEventStatus_eIgnore;
+  aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status);
+}
+
 uint32_t
 nsCoreUtils::GetAccessKeyFor(nsIContent* aContent)
 {
   // Accesskeys are registered by @accesskey attribute only. At first check
   // whether it is presented on the given element to avoid the slow
   // nsEventStateManager::GetRegisteredAccessKey() method.
   if (!aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::accesskey))
     return 0;
--- a/accessible/src/base/nsCoreUtils.h
+++ b/accessible/src/base/nsCoreUtils.h
@@ -44,39 +44,42 @@ public:
    */
   static void DispatchClickEvent(nsITreeBoxObject *aTreeBoxObj,
                                  int32_t aRowIndex, nsITreeColumn *aColumn,
                                  const nsCString& aPseudoElt = EmptyCString());
 
   /**
    * Send mouse event to the given element.
    *
-   * @param  aEventType  [in] an event type (see nsGUIEvent.h for constants)
-   * @param  aPresShell  [in] the presshell for the given element
-   * @param  aContent    [in] the element
-   */
-  static bool DispatchMouseEvent(uint32_t aEventType,
-                                   nsIPresShell *aPresShell,
-                                   nsIContent *aContent);
-
-  /**
-   * Send mouse event to the given element.
-   *
    * @param aEventType   [in] an event type (see nsGUIEvent.h for constants)
    * @param aX           [in] x coordinate in dev pixels
    * @param aY           [in] y coordinate in dev pixels
    * @param aContent     [in] the element
    * @param aFrame       [in] frame of the element
    * @param aPresShell   [in] the presshell for the element
    * @param aRootWidget  [in] the root widget of the element
    */
   static void DispatchMouseEvent(uint32_t aEventType, int32_t aX, int32_t aY,
                                  nsIContent *aContent, nsIFrame *aFrame,
-                                 nsIPresShell *aPresShell,
-                                 nsIWidget *aRootWidget);
+                                 nsIPresShell *aPresShell, nsIWidget *aRootWidget);
+
+  /**
+   * Send a touch event with a single touch point to the given element.
+   *
+   * @param aEventType   [in] an event type (see nsGUIEvent.h for constants)
+   * @param aX           [in] x coordinate in dev pixels
+   * @param aY           [in] y coordinate in dev pixels
+   * @param aContent     [in] the element
+   * @param aFrame       [in] frame of the element
+   * @param aPresShell   [in] the presshell for the element
+   * @param aRootWidget  [in] the root widget of the element
+   */
+  static void DispatchTouchEvent(uint32_t aEventType, int32_t aX, int32_t aY,
+                                 nsIContent* aContent, nsIFrame* aFrame,
+                                 nsIPresShell* aPresShell, nsIWidget* aRootWidget);
 
   /**
    * Return an accesskey registered on the given element by
    * nsEventStateManager or 0 if there is no registered accesskey.
    *
    * @param aContent - the given element.
    */
   static uint32_t GetAccessKeyFor(nsIContent *aContent);
--- a/accessible/src/base/nsTextEquivUtils.cpp
+++ b/accessible/src/base/nsTextEquivUtils.cpp
@@ -50,29 +50,16 @@ nsTextEquivUtils::GetNameFromSubtree(Acc
     }
   }
 
   sInitiatorAcc = nullptr;
 
   return NS_OK;
 }
 
-void
-nsTextEquivUtils::GetTextEquivFromSubtree(Accessible* aAccessible,
-                                          nsString& aTextEquiv)
-{
-  aTextEquiv.Truncate();
-
-  uint32_t nameRule = GetRoleRule(aAccessible->Role());
-  if (nameRule & eNameFromSubtreeIfReqRule) {
-    AppendFromAccessibleChildren(aAccessible, &aTextEquiv);
-    aTextEquiv.CompressWhitespace();
-  }
-}
-
 nsresult
 nsTextEquivUtils::GetTextEquivFromIDRefs(Accessible* aAccessible,
                                          nsIAtom *aIDRefsAttr,
                                          nsAString& aTextEquiv)
 {
   aTextEquiv.Truncate();
 
   nsIContent* content = aAccessible->GetContent();
--- a/accessible/src/base/nsTextEquivUtils.h
+++ b/accessible/src/base/nsTextEquivUtils.h
@@ -50,21 +50,26 @@ public:
    * @param aAccessible [in] the given accessible
    * @param aName       [out] accessible name
    */
   static nsresult GetNameFromSubtree(Accessible* aAccessible,
                                      nsAString& aName);
 
   /**
    * Calculates text equivalent from the subtree. Similar to GetNameFromSubtree.
-   * The difference it returns not empty result for things like HTML p, i.e.
-   * if the role has eNameFromSubtreeIfReq rule.
+   * However it returns not empty result for things like HTML p.
    */
   static void GetTextEquivFromSubtree(Accessible* aAccessible,
-                                      nsString& aTextEquiv);
+                                      nsString& aTextEquiv)
+  {
+    aTextEquiv.Truncate();
+
+    AppendFromAccessibleChildren(aAccessible, &aTextEquiv);
+    aTextEquiv.CompressWhitespace();
+  }
 
   /**
    * Calculates text equivalent for the given accessible from its IDRefs
    * attribute (like aria-labelledby or aria-describedby).
    *
    * @param aAccessible  [in] the accessible text equivalent is computed for
    * @param aIDRefsAttr  [in] IDRefs attribute on DOM node of the accessible
    * @param aTextEquiv   [out] result text equivalent
--- a/accessible/src/generic/Accessible.cpp
+++ b/accessible/src/generic/Accessible.cpp
@@ -1676,16 +1676,22 @@ Accessible::Value(nsString& aValue)
     if (!mContent->GetAttr(kNameSpaceID_None,
                            nsGkAtoms::aria_valuetext, aValue)) {
       mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow,
                         aValue);
     }
     return;
   }
 
+  // Value of textbox is a textified subtree.
+  if (mRoleMapEntry->Is(nsGkAtoms::textbox)) {
+    nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
+    return;
+  }
+
   // Value of combobox is a text of current or selected item.
   if (mRoleMapEntry->Is(nsGkAtoms::combobox)) {
     Accessible* option = CurrentItem();
     if (!option) {
       Accessible* listbox = nullptr;
       IDRefsIterator iter(mDoc, mContent, nsGkAtoms::aria_owns);
       while ((listbox = iter.Next()) && !listbox->IsListControl());
 
@@ -2282,23 +2288,38 @@ Accessible::DispatchClickEvent(nsIConten
   nsIPresShell* presShell = mDoc->PresShell();
 
   // Scroll into view.
   presShell->ScrollContentIntoView(aContent,
                                    nsIPresShell::ScrollAxis(),
                                    nsIPresShell::ScrollAxis(),
                                    nsIPresShell::SCROLL_OVERFLOW_HIDDEN);
 
-  // Fire mouse down and mouse up events.
-  bool res = nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_DOWN, presShell,
-                                               aContent);
-  if (!res)
+  nsIFrame* frame = aContent->GetPrimaryFrame();
+  if (!frame)
+    return;
+
+  // Compute x and y coordinates.
+  nsPoint point;
+  nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point);
+  if (!widget)
     return;
 
-  nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_UP, presShell, aContent);
+  nsSize size = frame->GetSize();
+
+  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);
 }
 
 NS_IMETHODIMP
 Accessible::ScrollTo(uint32_t aHow)
 {
   if (IsDefunct())
     return NS_ERROR_FAILURE;
 
--- a/accessible/src/generic/DocAccessible.cpp
+++ b/accessible/src/generic/DocAccessible.cpp
@@ -1260,29 +1260,53 @@ DocAccessible::GetAccessibleByUniqueIDIn
     if (child)
       return child;
   }
 
   return nullptr;
 }
 
 Accessible*
-DocAccessible::GetAccessibleOrContainer(nsINode* aNode)
+DocAccessible::GetAccessibleOrContainer(nsINode* aNode) const
 {
   if (!aNode || !aNode->IsInDoc())
     return nullptr;
 
   nsINode* currNode = aNode;
   Accessible* accessible = nullptr;
   while (!(accessible = GetAccessible(currNode)) &&
          (currNode = currNode->GetParentNode()));
 
   return accessible;
 }
 
+Accessible*
+DocAccessible::GetAccessibleOrDescendant(nsINode* aNode) const
+{
+  Accessible* acc = GetAccessible(aNode);
+  if (acc)
+    return acc;
+
+  acc = GetContainerAccessible(aNode);
+  if (acc) {
+    uint32_t childCnt = acc->ChildCount();
+    for (uint32_t idx = 0; idx < childCnt; idx++) {
+      Accessible* child = acc->GetChildAt(idx);
+      for (nsIContent* elm = child->GetContent();
+           elm && elm != acc->GetContent();
+           elm = elm->GetFlattenedTreeParent()) {
+        if (elm == aNode)
+          return child;
+      }
+    }
+  }
+
+  return nullptr;
+}
+
 bool
 DocAccessible::BindToDocument(Accessible* aAccessible,
                               nsRoleMapEntry* aRoleMapEntry)
 {
   if (!aAccessible)
     return false;
 
   // Put into DOM node cache.
--- a/accessible/src/generic/DocAccessible.h
+++ b/accessible/src/generic/DocAccessible.h
@@ -237,27 +237,32 @@ public:
    * this and nested documents.
    */
   Accessible* GetAccessibleByUniqueIDInSubtree(void* aUniqueID);
 
   /**
    * Return an accessible for the given DOM node or container accessible if
    * the node is not accessible.
    */
-  Accessible* GetAccessibleOrContainer(nsINode* aNode);
+  Accessible* GetAccessibleOrContainer(nsINode* aNode) const;
 
   /**
    * Return a container accessible for the given DOM node.
    */
-  Accessible* GetContainerAccessible(nsINode* aNode)
+  Accessible* GetContainerAccessible(nsINode* aNode) const
   {
     return aNode ? GetAccessibleOrContainer(aNode->GetParentNode()) : nullptr;
   }
 
   /**
+   * Return an accessible for the given node or its first accessible descendant.
+   */
+  Accessible* GetAccessibleOrDescendant(nsINode* aNode) const;
+
+  /**
    * Return true if the given ID is referred by relation attribute.
    *
    * @note Different elements may share the same ID if they are hosted inside
    *       XBL bindings. Be careful the result of this method may be  senseless
    *       while it's called for XUL elements (where XBL is used widely).
    */
   bool IsDependentID(const nsAString& aID) const
     { return mDependentIDsHash.Get(aID, nullptr); }
--- a/accessible/src/generic/HyperTextAccessible.cpp
+++ b/accessible/src/generic/HyperTextAccessible.cpp
@@ -713,43 +713,29 @@ HyperTextAccessible::GetRelativeOffset(n
     rv = RenderedToContentOffset(frame, aFromOffset, &contentOffset);
     NS_ENSURE_SUCCESS(rv, -1);
   }
 
   nsPeekOffsetStruct pos(aAmount, aDirection, contentOffset,
                          0, kIsJumpLinesOk, kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi,
                          aWordMovementType);
   rv = aFromFrame->PeekOffset(&pos);
-  if (NS_FAILED(rv)) {
-    pos.mResultContent = aFromFrame->GetContent();
-    if (aDirection == eDirPrevious) {
-      // Use passed-in frame as starting point in failure case for now,
-      // this is a hack to deal with starting on a list bullet frame,
-      // which fails in PeekOffset() because the line iterator doesn't see it.
-      // XXX Need to look at our overall handling of list bullets, which are an odd case
-      int32_t endOffsetUnused;
-      aFromFrame->GetOffsets(pos.mContentOffset, endOffsetUnused);
-    }
-    else {
-      // XXX: PeekOffset fails on a last frame in the document for
-      // eSelectLine/eDirNext. DOM selection (up/down arrowing processing) has
-      // similar code to handle this case. One day it should be incorporated
-      // into PeekOffset.
-      int32_t startOffsetUnused;
-      aFromFrame->GetOffsets(startOffsetUnused, pos.mContentOffset);
-    }
+
+  // PeekOffset fails on last/first lines of the text in certain cases.
+  if (NS_FAILED(rv) && aAmount == eSelectLine) {
+    pos.mAmount = (aDirection == eDirNext) ? eSelectEndLine : eSelectBeginLine;
+    aFromFrame->PeekOffset(&pos);
   }
-
-  // Turn the resulting node and offset into a hyperTextOffset
-  int32_t hyperTextOffset;
   if (!pos.mResultContent)
     return -1;
 
+  // Turn the resulting node and offset into a hyperTextOffset
   // If finalAccessible is nullptr, then DOMPointToHypertextOffset() searched
   // through the hypertext children without finding the node/offset position.
+  int32_t hyperTextOffset;
   Accessible* finalAccessible =
     DOMPointToHypertextOffset(pos.mResultContent, pos.mContentOffset,
                               &hyperTextOffset, aDirection == eDirNext);
 
   if (!finalAccessible && aDirection == eDirPrevious) {
     // If we reached the end during search, this means we didn't find the DOM point
     // and we're actually at the start of the paragraph
     hyperTextOffset = 0;
@@ -927,23 +913,29 @@ HyperTextAccessible::GetTextBeforeOffset
     case BOUNDARY_LINE_START:
       if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
         offset = AdjustCaretOffset(offset);
 
       *aStartOffset = FindLineBoundary(offset, ePrevLineBegin);
       *aEndOffset = FindLineBoundary(offset, eThisLineBegin);
       return GetText(*aStartOffset, *aEndOffset, aText);
 
-    case BOUNDARY_LINE_END:
+    case BOUNDARY_LINE_END: {
       if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
         offset = AdjustCaretOffset(offset);
 
       *aEndOffset = FindLineBoundary(offset, ePrevLineEnd);
-      *aStartOffset = FindLineBoundary(*aEndOffset, ePrevLineEnd);
+      int32_t tmpOffset = *aEndOffset;
+      // Adjust offset if line is wrapped.
+      if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset))
+        tmpOffset--;
+
+      *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd);
       return GetText(*aStartOffset, *aEndOffset, aText);
+    }
 
     default:
       return NS_ERROR_INVALID_ARG;
   }
 }
 
 NS_IMETHODIMP
 HyperTextAccessible::GetTextAtOffset(int32_t aOffset,
@@ -2146,23 +2138,19 @@ HyperTextAccessible::RenderedToContentOf
 bool
 HyperTextAccessible::GetCharAt(int32_t aOffset, EGetTextType aShift,
                                nsAString& aChar, int32_t* aStartOffset,
                                int32_t* aEndOffset)
 {
   aChar.Truncate();
 
   int32_t offset = ConvertMagicOffset(aOffset) + static_cast<int32_t>(aShift);
-  int32_t childIdx = GetChildIndexAtOffset(offset);
-  if (childIdx == -1)
+  if (!CharAt(offset, aChar))
     return false;
 
-  Accessible* child = GetChildAt(childIdx);
-  child->AppendTextTo(aChar, offset - GetChildOffset(childIdx), 1);
-
   if (aStartOffset)
     *aStartOffset = offset;
   if (aEndOffset)
     *aEndOffset = aChar.IsEmpty() ? offset : offset + 1;
 
   return true;
 }
 
--- a/accessible/src/generic/HyperTextAccessible.h
+++ b/accessible/src/generic/HyperTextAccessible.h
@@ -160,16 +160,46 @@ public:
    * Return character count within the hypertext accessible.
    */
   uint32_t CharacterCount()
   {
     return GetChildOffset(ChildCount());
   }
 
   /**
+   * Get a character at the given offset (don't support magic offsets).
+   */
+  bool CharAt(int32_t aOffset, nsAString& aChar)
+  {
+    int32_t childIdx = GetChildIndexAtOffset(aOffset);
+    if (childIdx == -1)
+      return false;
+
+    Accessible* child = GetChildAt(childIdx);
+    child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1);
+    return true;
+  }
+
+  /**
+   * Return true if char at the given offset equals to given char.
+   */
+  bool IsCharAt(int32_t aOffset, char aChar)
+  {
+    nsAutoString charAtOffset;
+    CharAt(aOffset, charAtOffset);
+    return charAtOffset.CharAt(0) == aChar;
+  }
+
+  /**
+   * Return true if terminal char is at the given offset.
+   */
+  bool IsLineEndCharAt(int32_t aOffset)
+    { return IsCharAt(aOffset, '\n'); }
+
+  /**
    * Get a character before/at/after the given offset.
    *
    * @param aOffset       [in] the given offset
    * @param aShift        [in] specifies whether to get a char before/at/after
    *                        offset
    * @param aChar         [out] the character
    * @param aStartOffset  [out, optional] the start offset of the character
    * @param aEndOffset    [out, optional] the end offset of the character
@@ -283,22 +313,18 @@ protected:
     return aOffset;
   }
 
   /**
    * Return true if the given offset points to terminal empty line if any.
    */
   bool IsEmptyLastLineOffset(int32_t aOffset)
   {
-    if (aOffset != static_cast<int32_t>(CharacterCount()))
-      return false;
-
-    nsAutoString lastChar;
-    GetText(aOffset -1, -1, lastChar);
-    return lastChar.EqualsLiteral("\n");
+    return aOffset == static_cast<int32_t>(CharacterCount()) &&
+      IsLineEndCharAt(aOffset - 1);
   }
 
   /**
    * Return an offset of the found word boundary.
    */
   int32_t FindWordBoundary(int32_t aOffset, nsDirection aDirection,
                            EWordMovementType aWordMovementType)
   {
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -111,17 +111,17 @@ this.AccessFu = {
     TouchAdapter.start();
 
     Services.obs.addObserver(this, 'remote-browser-frame-shown', false);
     Services.obs.addObserver(this, 'in-process-browser-or-app-frame-shown', false);
     Services.obs.addObserver(this, 'Accessibility:NextObject', false);
     Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
     Services.obs.addObserver(this, 'Accessibility:Focus', false);
     Services.obs.addObserver(this, 'Accessibility:ActivateObject', false);
-    Services.obs.addObserver(this, 'Accessibility:MoveCaret', false);
+    Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false);
     Utils.win.addEventListener('TabOpen', this);
     Utils.win.addEventListener('TabClose', this);
     Utils.win.addEventListener('TabSelect', this);
 
     if (this.readyCallback) {
       this.readyCallback();
       delete this.readyCallback;
     }
@@ -154,17 +154,17 @@ this.AccessFu = {
     Utils.win.removeEventListener('TabSelect', this);
 
     Services.obs.removeObserver(this, 'remote-browser-frame-shown');
     Services.obs.removeObserver(this, 'in-process-browser-or-app-frame-shown');
     Services.obs.removeObserver(this, 'Accessibility:NextObject');
     Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
     Services.obs.removeObserver(this, 'Accessibility:Focus');
     Services.obs.removeObserver(this, 'Accessibility:ActivateObject');
-    Services.obs.removeObserver(this, 'Accessibility:MoveCaret');
+    Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
 
     if (this.doneCallback) {
       this.doneCallback();
       delete this.doneCallback;
     }
   },
 
   _enableOrDisable: function _enableOrDisable() {
@@ -196,16 +196,19 @@ this.AccessFu = {
         this._output(aMessage.json, aMessage.target);
         break;
       case 'AccessFu:Input':
         this.Input.setEditState(aMessage.json);
         break;
       case 'AccessFu:ActivateContextMenu':
         this.Input.activateContextMenu(aMessage.json);
         break;
+      case 'AccessFu:DoScroll':
+        this.Input.doScroll(aMessage.json);
+        break;
     }
   },
 
   _output: function _output(aPresentationData, aBrowser) {
     for each (let presenter in aPresentationData) {
       if (!presenter)
         continue;
 
@@ -235,23 +238,25 @@ this.AccessFu = {
     }
   },
 
   _addMessageListeners: function _addMessageListeners(aMessageManager) {
     aMessageManager.addMessageListener('AccessFu:Present', this);
     aMessageManager.addMessageListener('AccessFu:Input', this);
     aMessageManager.addMessageListener('AccessFu:Ready', this);
     aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this);
+    aMessageManager.addMessageListener('AccessFu:DoScroll', this);
   },
 
   _removeMessageListeners: function _removeMessageListeners(aMessageManager) {
     aMessageManager.removeMessageListener('AccessFu:Present', this);
     aMessageManager.removeMessageListener('AccessFu:Input', this);
     aMessageManager.removeMessageListener('AccessFu:Ready', this);
     aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this);
+    aMessageManager.removeMessageListener('AccessFu:DoScroll', this);
   },
 
   _handleMessageManager: function _handleMessageManager(aMessageManager) {
     if (this._enabled) {
       this._addMessageListeners(aMessageManager);
     }
     this._loadFrameScript(aMessageManager);
   },
@@ -272,18 +277,18 @@ this.AccessFu = {
         this.Input.activateCurrent(JSON.parse(aData));
         break;
       case 'Accessibility:Focus':
         this._focused = JSON.parse(aData);
         if (this._focused) {
           this.showCurrent(true);
         }
         break;
-      case 'Accessibility:MoveCaret':
-        this.Input.moveCaret(JSON.parse(aData));
+      case 'Accessibility:MoveByGranularity':
+        this.Input.moveByGranularity(JSON.parse(aData));
         break;
       case 'remote-browser-frame-shown':
       case 'in-process-browser-or-app-frame-shown':
       {
         let mm = aSubject.QueryInterface(Ci.nsIFrameLoader).messageManager;
         this._handleMessageManager(mm);
         break;
       }
@@ -648,41 +653,41 @@ var Input = {
   _handleGesture: function _handleGesture(aGesture) {
     let gestureName = aGesture.type + aGesture.touches.length;
     Logger.info('Gesture', aGesture.type,
                 '(fingers: ' + aGesture.touches.length + ')');
 
     switch (gestureName) {
       case 'dwell1':
       case 'explore1':
-        this.moveToPoint('SimpleTouch', aGesture.x, aGesture.y);
+        this.moveToPoint('Simple', aGesture.x, aGesture.y);
         break;
       case 'doubletap1':
         this.activateCurrent();
         break;
       case 'doubletaphold1':
         this.sendContextMenuMessage();
         break;
       case 'swiperight1':
         this.moveCursor('moveNext', 'Simple', 'gestures');
         break;
       case 'swipeleft1':
         this.moveCursor('movePrevious', 'Simple', 'gesture');
         break;
       case 'swiperight2':
-        this.scroll(-1, true);
+        this.sendScrollMessage(-1, true);
         break;
       case 'swipedown2':
-        this.scroll(-1);
+        this.sendScrollMessage(-1);
         break;
       case 'swipeleft2':
-        this.scroll(1, true);
+        this.sendScrollMessage(1, true);
         break;
       case 'swipeup2':
-        this.scroll(1);
+        this.sendScrollMessage(1);
         break;
       case 'explore2':
         Utils.CurrentBrowser.contentWindow.scrollBy(
           -aGesture.deltaX, -aGesture.deltaY);
         break;
       case 'swiperight3':
         this.moveCursor('moveNext', this.quickNavMode.current, 'gesture');
         break;
@@ -783,59 +788,84 @@ var Input = {
 
   moveCursor: function moveCursor(aAction, aRule, aInputType) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:MoveCursor',
                         {action: aAction, rule: aRule,
                          origin: 'top', inputType: aInputType});
   },
 
-  moveCaret: function moveCaret(aDetails) {
+  moveByGranularity: function moveByGranularity(aDetails) {
+    const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
+
     if (!this.editState.editing) {
-      return;
+      if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) {
+        this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture');
+        return;
+      }
+    } else {
+      aDetails.atStart = this.editState.atStart;
+      aDetails.atEnd = this.editState.atEnd;
     }
 
-    aDetails.atStart = this.editState.atStart;
-    aDetails.atEnd = this.editState.atEnd;
-
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
-    mm.sendAsyncMessage('AccessFu:MoveCaret', aDetails);
+    let type = this.editState.editing ? 'AccessFu:MoveCaret' :
+                                        'AccessFu:MoveByGranularity';
+    mm.sendAsyncMessage(type, aDetails);
   },
 
   activateCurrent: function activateCurrent(aData) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     let offset = aData && typeof aData.keyIndex === 'number' ?
                  aData.keyIndex - Output.brailleState.startOffset : -1;
 
     mm.sendAsyncMessage('AccessFu:Activate', {offset: offset});
   },
 
   sendContextMenuMessage: function sendContextMenuMessage() {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:ContextMenu', {});
   },
 
-  activateContextMenu: function activateContextMenu(aMessage) {
+  activateContextMenu: function activateContextMenu(aDetails) {
     if (Utils.MozBuildApp === 'mobile/android') {
-      let p = AccessFu.adjustContentBounds(aMessage.bounds, Utils.CurrentBrowser,
+      let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
                                            true, true).center();
       Services.obs.notifyObservers(null, 'Gesture:LongPress',
                                    JSON.stringify({x: p.x, y: p.y}));
     }
   },
 
   setEditState: function setEditState(aEditState) {
     this.editState = aEditState;
   },
 
+  // XXX: This is here for backwards compatability with screen reader simulator
+  // it should be removed when the extension is updated on amo.
   scroll: function scroll(aPage, aHorizontal) {
+    this.sendScrollMessage(aPage, aHorizontal);
+  },
+
+  sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'});
   },
 
+  doScroll: function doScroll(aDetails) {
+    let horizontal = aDetails.horizontal;
+    let page = aDetails.page;
+    let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
+                                         true, true).center();
+    let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor).
+      getInterface(Ci.nsIDOMWindowUtils);
+    wu.sendWheelEvent(p.x, p.y,
+                      horizontal ? page : 0, horizontal ? 0 : page, 0,
+                      Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0);
+  },
+
   get keyMap() {
     delete this.keyMap;
     this.keyMap = {
       a: ['moveNext', 'Anchor'],
       A: ['movePrevious', 'Anchor'],
       b: ['moveNext', 'Button'],
       B: ['movePrevious', 'Button'],
       c: ['moveNext', 'Combobox'],
--- a/accessible/src/jsat/EventManager.jsm
+++ b/accessible/src/jsat/EventManager.jsm
@@ -151,21 +151,22 @@ this.EventManager.prototype = {
           QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
         let position = pivot.position;
         if (position && position.role == ROLE_INTERNAL_FRAME)
           break;
         let event = aEvent.
           QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
         let reason = event.reason;
 
-        if (this.editState.editing)
+        if (this.editState.editing) {
           aEvent.accessibleDocument.takeFocus();
-
+        }
         this.present(
-          Presentation.pivotChanged(position, event.oldAccessible, reason));
+          Presentation.pivotChanged(position, event.oldAccessible, reason,
+                                    pivot.startOffset, pivot.endOffset));
 
         break;
       }
       case EVENT_STATE_CHANGE:
       {
         let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
         if (event.state == Ci.nsIAccessibleStates.STATE_CHECKED &&
             !(event.isExtraState)) {
--- a/accessible/src/jsat/OutputGenerator.jsm
+++ b/accessible/src/jsat/OutputGenerator.jsm
@@ -159,16 +159,29 @@ this.OutputGenerator = {
 
   _addName: function _addName(aOutput, aAccessible, aFlags) {
     let name;
     if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
       (aFlags & INCLUDE_NAME)) {
       name = aAccessible.name;
     }
 
+    let description = aAccessible.description;
+    if (description) {
+      // Compare against the calculated name unconditionally, regardless of name rule,
+      // so we can make sure we don't speak duplicated descriptions
+      let tmpName = name || aAccessible.name;
+      if (tmpName && (description !== tmpName)) {
+        name = name || '';
+        name = this.outputOrder === OUTPUT_DESC_FIRST ?
+          description + ' - ' + name :
+          name + ' - ' + description;
+      }
+    }
+
     if (name) {
       aOutput[this.outputOrder === OUTPUT_DESC_FIRST ?
         'push' : 'unshift'](name);
     }
   },
 
   /**
    * Adds a landmark role to the output if available.
--- a/accessible/src/jsat/Presentation.jsm
+++ b/accessible/src/jsat/Presentation.jsm
@@ -119,47 +119,63 @@ VisualPresenter.prototype = {
   type: 'Visual',
 
   /**
    * The padding in pixels between the object and the highlight border.
    */
   BORDER_PADDING: 2,
 
   viewportChanged: function VisualPresenter_viewportChanged(aWindow) {
-    let currentAcc = this._displayedAccessibles.get(aWindow);
+    let currentDisplay = this._displayedAccessibles.get(aWindow);
+    if (!currentDisplay) {
+      return null;
+    }
+
+    let currentAcc = currentDisplay.accessible;
+    let start = currentDisplay.startOffset;
+    let end = currentDisplay.endOffset;
     if (Utils.isAliveAndVisible(currentAcc)) {
-      let bounds = Utils.getBounds(currentAcc);
+      let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) :
+                   Utils.getTextBounds(currentAcc, start, end);
+
       return {
         type: this.type,
         details: {
           method: 'showBounds',
           bounds: bounds,
           padding: this.BORDER_PADDING
         }
       };
     }
 
     return null;
   },
 
   pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
     this._displayedAccessibles.set(aContext.accessible.document.window,
-                                   aContext.accessible);
+                                   { accessible: aContext.accessible,
+                                     startOffset: aContext.startOffset,
+                                     endOffset: aContext.endOffset });
 
     if (!aContext.accessible)
       return {type: this.type, details: {method: 'hideBounds'}};
 
     try {
       aContext.accessible.scrollTo(
         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
+
+      let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ?
+                   aContext.bounds : Utils.getTextBounds(aContext.accessible,
+                                     aContext.startOffset, aContext.endOffset);
+
       return {
         type: this.type,
         details: {
           method: 'showBounds',
-          bounds: aContext.bounds,
+          bounds: bounds,
           padding: this.BORDER_PADDING
         }
       };
     } catch (e) {
       Logger.logException(e, 'Failed to get bounds');
       return null;
     }
   },
@@ -227,37 +243,49 @@ AndroidPresenter.prototype = {
       this.ANDROID_VIEW_FOCUSED;
 
     if (isExploreByTouch) {
       // This isn't really used by TalkBack so this is a half-hearted attempt
       // for now.
       androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
     }
 
-    let state = Utils.getStates(aContext.accessible)[0];
-
     let brailleOutput = {};
     if (Utils.AndroidSdkVersion >= 16) {
       if (!this._braillePresenter) {
         this._braillePresenter = new BraillePresenter();
       }
       brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
                          details;
     }
 
-    androidEvents.push({eventType: (isExploreByTouch) ?
-                          this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
-                        text: UtteranceGenerator.genForContext(aContext).output,
-                        bounds: aContext.bounds,
-                        clickable: aContext.accessible.actionCount > 0,
-                        checkable: !!(state &
-                                      Ci.nsIAccessibleStates.STATE_CHECKABLE),
-                        checked: !!(state &
-                                    Ci.nsIAccessibleStates.STATE_CHECKED),
-                        brailleOutput: brailleOutput});
+    if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
+      if (Utils.AndroidSdkVersion >= 16) {
+        let adjustedText = aContext.textAndAdjustedOffsets;
+
+        androidEvents.push({
+          eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+          text: [adjustedText.text],
+          fromIndex: adjustedText.startOffset,
+          toIndex: adjustedText.endOffset
+        });
+      }
+    } else {
+      let state = Utils.getStates(aContext.accessible)[0];
+      androidEvents.push({eventType: (isExploreByTouch) ?
+                           this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
+                         text: UtteranceGenerator.genForContext(aContext).output,
+                         bounds: aContext.bounds,
+                         clickable: aContext.accessible.actionCount > 0,
+                         checkable: !!(state &
+                                       Ci.nsIAccessibleStates.STATE_CHECKABLE),
+                         checked: !!(state &
+                                     Ci.nsIAccessibleStates.STATE_CHECKED),
+                         brailleOutput: brailleOutput});
+    }
 
 
     return {
       type: this.type,
       details: androidEvents
     };
   },
 
@@ -489,20 +517,19 @@ this.Presentation = {
     } else {
       this.presenters.push(new SpeechPresenter());
       this.presenters.push(new HapticPresenter());
     }
 
     return this.presenters;
   },
 
-  pivotChanged: function Presentation_pivotChanged(aPosition,
-                                                   aOldPosition,
-                                                   aReason) {
-    let context = new PivotContext(aPosition, aOldPosition);
+  pivotChanged: function Presentation_pivotChanged(aPosition, aOldPosition, aReason,
+                                                   aStartOffset, aEndOffset) {
+    let context = new PivotContext(aPosition, aOldPosition, aStartOffset, aEndOffset);
     return [p.pivotChanged(context, aReason)
               for each (p in this.presenters)];
   },
 
   actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
     return [p.actionInvoked(aObject, aActionName)
               for each (p in this.presenters)];
   },
--- a/accessible/src/jsat/TouchAdapter.jsm
+++ b/accessible/src/jsat/TouchAdapter.jsm
@@ -37,16 +37,19 @@ this.TouchAdapter = {
   DWELL_REPEAT_DELAY: 300,
 
   // maximum distance the mouse could move during a tap in inches
   TAP_MAX_RADIUS: 0.2,
 
   // The virtual touch ID generated by a mouse event.
   MOUSE_ID: 'mouse',
 
+  // Synthesized touch ID.
+  SYNTH_ID: -1,
+
   start: function TouchAdapter_start() {
     Logger.info('TouchAdapter.start');
 
     this._touchPoints = {};
     this._dwellTimeout = 0;
     this._prevGestures = {};
     this._lastExploreTime = 0;
     this._dpi = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor).
@@ -126,29 +129,34 @@ this.TouchAdapter = {
         aEvent.view.top instanceof Ci.nsIDOMChromeWindow) {
       return;
     }
 
     if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN) {
       return;
     }
 
+    let changedTouches = aEvent.changedTouches || [aEvent];
+
+    if (changedTouches.length == 1 &&
+        changedTouches[0].identifier == this.SYNTH_ID) {
+      return;
+    }
+
     if (!this.eventsOfInterest[aEvent.type]) {
       aEvent.preventDefault();
       aEvent.stopImmediatePropagation();
       return;
     }
 
     if (this._delayedEvent) {
       Utils.win.clearTimeout(this._delayedEvent);
       delete this._delayedEvent;
     }
 
-    let changedTouches = aEvent.changedTouches || [aEvent];
-
     // XXX: Until bug 77992 is resolved, on desktop we get microseconds
     // instead of milliseconds.
     let timeStamp = (Utils.OS == 'Android') ? aEvent.timeStamp : Date.now();
     switch (aEvent.type) {
       case 'mousedown':
       case 'mouseenter':
       case 'touchstart':
         for (var i = 0; i < changedTouches.length; i++) {
--- a/accessible/src/jsat/TraversalRules.jsm
+++ b/accessible/src/jsat/TraversalRules.jsm
@@ -34,20 +34,23 @@ const ROLE_TOGGLE_BUTTON = Ci.nsIAccessi
 const ROLE_ENTRY = Ci.nsIAccessibleRole.ROLE_ENTRY;
 const ROLE_LIST = Ci.nsIAccessibleRole.ROLE_LIST;
 const ROLE_DEFINITION_LIST = Ci.nsIAccessibleRole.ROLE_DEFINITION_LIST;
 const ROLE_LISTITEM = Ci.nsIAccessibleRole.ROLE_LISTITEM;
 const ROLE_BUTTONDROPDOWNGRID = Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID;
 const ROLE_LISTBOX = Ci.nsIAccessibleRole.ROLE_LISTBOX;
 const ROLE_SLIDER = Ci.nsIAccessibleRole.ROLE_SLIDER;
 const ROLE_HEADING = Ci.nsIAccessibleRole.ROLE_HEADING;
+const ROLE_HEADER = Ci.nsIAccessibleRole.ROLE_HEADER;
 const ROLE_TERM = Ci.nsIAccessibleRole.ROLE_TERM;
 const ROLE_SEPARATOR = Ci.nsIAccessibleRole.ROLE_SEPARATOR;
 const ROLE_TABLE = Ci.nsIAccessibleRole.ROLE_TABLE;
 const ROLE_INTERNAL_FRAME = Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME;
+const ROLE_PARAGRAPH = Ci.nsIAccessibleRole.ROLE_PARAGRAPH;
+const ROLE_SECTION = Ci.nsIAccessibleRole.ROLE_SECTION;
 
 this.EXPORTED_SYMBOLS = ['TraversalRules'];
 
 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
 let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images');
 
@@ -96,73 +99,75 @@ var gSimpleTraversalRoles =
    ROLE_PROGRESSBAR,
    ROLE_BUTTONDROPDOWN,
    ROLE_BUTTONMENU,
    ROLE_CHECK_MENU_ITEM,
    ROLE_PASSWORD_TEXT,
    ROLE_RADIO_MENU_ITEM,
    ROLE_TOGGLE_BUTTON,
    ROLE_ENTRY,
+   ROLE_HEADER,
+   ROLE_HEADING,
    // Used for traversing in to child OOP frames.
    ROLE_INTERNAL_FRAME];
 
 this.TraversalRules = {
   Simple: new BaseTraversalRule(
     gSimpleTraversalRoles,
     function Simple_match(aAccessible) {
+      function hasZeroOrSingleChildDescendants () {
+        for (let acc = aAccessible; acc.childCount > 0; acc = acc.firstChild) {
+          if (acc.childCount > 1) {
+            return false;
+          }
+        }
+
+        return true;
+      }
+
       switch (aAccessible.role) {
       case ROLE_COMBOBOX:
         // We don't want to ignore the subtree because this is often
         // where the list box hangs out.
         return FILTER_MATCH;
       case ROLE_TEXT_LEAF:
         {
           // Nameless text leaves are boring, skip them.
           let name = aAccessible.name;
           if (name && name.trim())
             return FILTER_MATCH;
           else
             return FILTER_IGNORE;
         }
-      case ROLE_LINK:
-        // If the link has children we should land on them instead.
-        // Image map links don't have children so we need to match those.
-        if (aAccessible.childCount == 0)
-          return FILTER_MATCH;
-        else
-          return FILTER_IGNORE;
       case ROLE_STATICTEXT:
         {
           let parent = aAccessible.parent;
           // Ignore prefix static text in list items. They are typically bullets or numbers.
           if (parent.childCount > 1 && aAccessible.indexInParent == 0 &&
               parent.role == ROLE_LISTITEM)
             return FILTER_IGNORE;
 
           return FILTER_MATCH;
         }
       case ROLE_GRAPHIC:
         return TraversalRules._shouldSkipImage(aAccessible);
+      case ROLE_LINK:
+      case ROLE_HEADER:
+      case ROLE_HEADING:
+        return hasZeroOrSingleChildDescendants() ?
+          (FILTER_MATCH | FILTER_IGNORE_SUBTREE) : (FILTER_IGNORE);
       default:
         // Ignore the subtree, if there is one. So that we don't land on
         // the same content that was already presented by its parent.
         return FILTER_MATCH |
           FILTER_IGNORE_SUBTREE;
       }
     }
   ),
 
-  SimpleTouch: new BaseTraversalRule(
-    gSimpleTraversalRoles,
-    function Simple_match(aAccessible) {
-      return FILTER_MATCH |
-        FILTER_IGNORE_SUBTREE;
-    }
-  ),
-
   Anchor: new BaseTraversalRule(
     [ROLE_LINK],
     function Anchor_match(aAccessible)
     {
       // We want to ignore links, only focus named anchors.
       let state = {};
       let extraState = {};
       aAccessible.getState(state, extraState);
@@ -243,16 +248,29 @@ this.TraversalRules = {
 
   List: new BaseTraversalRule(
     [ROLE_LIST,
      ROLE_DEFINITION_LIST]),
 
   PageTab: new BaseTraversalRule(
     [ROLE_PAGETAB]),
 
+  Paragraph: new BaseTraversalRule(
+    [ROLE_PARAGRAPH,
+     ROLE_SECTION],
+    function Paragraph_match(aAccessible) {
+      for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
+        if (child.role === ROLE_TEXT_LEAF) {
+          return FILTER_MATCH | FILTER_IGNORE_SUBTREE;
+        }
+      }
+
+      return FILTER_IGNORE;
+    }),
+
   RadioButton: new BaseTraversalRule(
     [ROLE_RADIOBUTTON,
      ROLE_RADIO_MENU_ITEM]),
 
   Separator: new BaseTraversalRule(
     [ROLE_SEPARATOR]),
 
   Table: new BaseTraversalRule(
--- a/accessible/src/jsat/Utils.jsm
+++ b/accessible/src/jsat/Utils.jsm
@@ -222,16 +222,24 @@ this.Utils = {
   },
 
   getBounds: function getBounds(aAccessible) {
       let objX = {}, objY = {}, objW = {}, objH = {};
       aAccessible.getBounds(objX, objY, objW, objH);
       return new Rect(objX.value, objY.value, objW.value, objH.value);
   },
 
+  getTextBounds: function getTextBounds(aAccessible, aStart, aEnd) {
+      let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
+      let objX = {}, objY = {}, objW = {}, objH = {};
+      accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH,
+                              Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE);
+      return new Rect(objX.value, objY.value, objW.value, objH.value);
+  },
+
   inHiddenSubtree: function inHiddenSubtree(aAccessible) {
     for (let acc=aAccessible; acc; acc=acc.parent) {
       let hidden = Utils.getAttributes(acc).hidden;
       if (hidden && JSON.parse(hidden)) {
         return true;
       }
     }
     return false;
@@ -408,31 +416,73 @@ this.Logger = {
       this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i), aIndent + 1);
     }
 };
 
 /**
  * PivotContext: An object that generates and caches context information
  * for a given accessible and its relationship with another accessible.
  */
-this.PivotContext = function PivotContext(aAccessible, aOldAccessible) {
+this.PivotContext = function PivotContext(aAccessible, aOldAccessible,
+                                          aStartOffset, aEndOffset) {
   this._accessible = aAccessible;
   this._oldAccessible =
     this._isDefunct(aOldAccessible) ? null : aOldAccessible;
+  this.startOffset = aStartOffset;
+  this.endOffset = aEndOffset;
 }
 
 PivotContext.prototype = {
   get accessible() {
     return this._accessible;
   },
 
   get oldAccessible() {
     return this._oldAccessible;
   },
 
+  get textAndAdjustedOffsets() {
+    if (this.startOffset === -1 && this.endOffset === -1) {
+      return null;
+    }
+
+    if (!this._textAndAdjustedOffsets) {
+      let result = {startOffset: this.startOffset,
+                    endOffset: this.endOffset,
+                    text: this._accessible.QueryInterface(Ci.nsIAccessibleText).
+                          getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)};
+      let hypertextAcc = this._accessible.QueryInterface(Ci.nsIAccessibleHyperText);
+
+      // Iterate through the links in backwards order so text replacements don't
+      // affect the offsets of links yet to be processed.
+      for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) {
+        let link = hypertextAcc.getLinkAt(i);
+        let linkText = '';
+        if (link instanceof Ci.nsIAccessibleText) {
+          linkText = link.QueryInterface(Ci.nsIAccessibleText).
+                          getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
+        }
+
+        let start = link.startIndex;
+        let end = link.endIndex;
+        for (let offset of ['startOffset', 'endOffset']) {
+          if (this[offset] >= end) {
+            result[offset] += linkText.length - (end - start);
+          }
+        }
+        result.text = result.text.substring(0, start) + linkText +
+                      result.text.substring(end);
+      }
+
+      this._textAndAdjustedOffsets = result;
+    }
+
+    return this._textAndAdjustedOffsets;
+  },
+
   /**
    * Get a list of |aAccessible|'s ancestry up to the root.
    * @param  {nsIAccessible} aAccessible.
    * @return {Array} Ancestry list.
    */
   _getAncestry: function _getAncestry(aAccessible) {
     let ancestry = [];
     let parent = aAccessible;
--- a/accessible/src/jsat/content-script.js
+++ b/accessible/src/jsat/content-script.js
@@ -3,16 +3,20 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 const ROLE_ENTRY = Ci.nsIAccessibleRole.ROLE_ENTRY;
 const ROLE_INTERNAL_FRAME = Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME;
 
+const MOVEMENT_GRANULARITY_CHARACTER = 1;
+const MOVEMENT_GRANULARITY_WORD = 2;
+const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
+
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
   'resource://gre/modules/accessibility/Presentation.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
   'resource://gre/modules/accessibility/TraversalRules.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
@@ -112,17 +116,18 @@ function showCurrent(aMessage) {
 
   let vc = Utils.getVirtualCursor(content.document);
 
   if (!forwardToChild(vc, showCurrent, aMessage)) {
     if (!vc.position && aMessage.json.move) {
       vc.moveFirst(TraversalRules.Simple);
     } else {
       sendAsyncMessage('AccessFu:Present', Presentation.pivotChanged(
-                         vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE));
+                         vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE,
+                         vc.startOffset, vc.endOffset));
     }
   }
 }
 
 function forwardToParent(aMessage) {
   // XXX: This is a silly way to make a deep copy
   let newJSON = JSON.parse(JSON.stringify(aMessage.json));
   newJSON.origin = 'child';
@@ -221,21 +226,40 @@ function activateContextMenu(aMessage) {
   }
 
   let position = Utils.getVirtualCursor(content.document).position;
   if (!forwardToChild(aMessage, activateContextMenu, position)) {
     sendContextMenuCoordinates(position);
   }
 }
 
+function moveByGranularity(aMessage) {
+  let direction = aMessage.json.direction;
+  let vc = Utils.getVirtualCursor(content.document);
+  let granularity;
+
+  switch(aMessage.json.granularity) {
+    case MOVEMENT_GRANULARITY_CHARACTER:
+      granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY;
+      break;
+    case MOVEMENT_GRANULARITY_WORD:
+      granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY;
+      break;
+    default:
+      return;
+  }
+
+  if (direction === 'Previous') {
+    vc.movePreviousByText(granularity);
+  } else if (direction === 'Next') {
+    vc.moveNextByText(granularity);
+  }
+}
+
 function moveCaret(aMessage) {
-  const MOVEMENT_GRANULARITY_CHARACTER = 1;
-  const MOVEMENT_GRANULARITY_WORD = 2;
-  const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
-
   let direction = aMessage.json.direction;
   let granularity = aMessage.json.granularity;
   let accessible = Utils.getVirtualCursor(content.document).position;
   let accText = accessible.QueryInterface(Ci.nsIAccessibleText);
   let oldOffset = accText.caretOffset;
   let text = accText.getText(0, accText.characterCount);
 
   let start = {}, end = {};
@@ -277,90 +301,27 @@ function presentCaretChange(aText, aOldO
   if (aOldOffset !== aNewOffset) {
     let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
                                                 aOldOffset, aOldOffset, true);
     sendAsyncMessage('AccessFu:Present', msg);
   }
 }
 
 function scroll(aMessage) {
-  let vc = Utils.getVirtualCursor(content.document);
-
-  function tryToScroll() {
-    let horiz = aMessage.json.horizontal;
-    let page = aMessage.json.page;
-
-    // Search up heirarchy for scrollable element.
-    let acc = vc.position;
-    while (acc) {
-      let elem = acc.DOMNode;
-
-      // This is inspired by IndieUI events. Once they are
-      // implemented, it should be easy to transition to them.
-      // https://dvcs.w3.org/hg/IndieUI/raw-file/tip/src/indie-ui-events.html#scrollrequest
-      let uiactions = elem.getAttribute ? elem.getAttribute('uiactions') : '';
-      if (uiactions && uiactions.split(' ').indexOf('scroll') >= 0) {
-        let evt = elem.ownerDocument.createEvent('CustomEvent');
-        let details = horiz ? { deltaX: page * elem.clientWidth } :
-          { deltaY: page * elem.clientHeight };
-        evt.initCustomEvent(
-          'scrollrequest', true, true,
-          ObjectWrapper.wrap(details, elem.ownerDocument.defaultView));
-        if (!elem.dispatchEvent(evt))
-          return;
-      }
-
-      // We will do window scrolling next.
-      if (elem == content.document)
-        break;
-
-      if (!horiz && elem.clientHeight < elem.scrollHeight) {
-        let s = content.getComputedStyle(elem);
-        if (s.overflowY == 'scroll' || s.overflowY == 'auto') {
-          elem.scrollTop += page * elem.clientHeight;
-          return true;
-        }
-      }
-
-      if (horiz) {
-        if (elem.clientWidth < elem.scrollWidth) {
-          let s = content.getComputedStyle(elem);
-          if (s.overflowX == 'scroll' || s.overflowX == 'auto') {
-            elem.scrollLeft += page * elem.clientWidth;
-            return true;
-          }
-        }
-      }
-      acc = acc.parent;
-    }
-
-    // Scroll window.
-    if (!horiz && content.scrollMaxY &&
-        ((page > 0 && content.scrollY < content.scrollMaxY) ||
-         (page < 0 && content.scrollY > 0))) {
-      content.scroll(0, content.innerHeight * page + content.scrollY);
-      return true;
-    } else if (horiz && content.scrollMaxX &&
-               ((page > 0 && content.scrollX < content.scrollMaxX) ||
-                (page < 0 && content.scrollX > 0))) {
-      content.scroll(content.innerWidth * page + content.scrollX);
-      return true;
-    }
-
-    return false;
+  function sendScrollCoordinates(aAccessible) {
+    let bounds = Utils.getBounds(aAccessible);
+    sendAsyncMessage('AccessFu:DoScroll',
+                     { bounds: bounds,
+                       page: aMessage.json.page,
+                       horizontal: aMessage.json.horizontal });
   }
 
-  if (aMessage.json.origin != 'child' &&
-      forwardToChild(aMessage, scroll, vc.position)) {
-    return;
-  }
-
-  if (!tryToScroll()) {
-    // Failed to scroll anything in this document. Try in parent document.
-    forwardToParent(aMessage);
+  let position = Utils.getVirtualCursor(content.document).position;
+  if (!forwardToChild(aMessage, scroll, position)) {
+    sendScrollCoordinates(position);
   }
 }
 
 addMessageListener(
   'AccessFu:Start',
   function(m) {
     Logger.debug('AccessFu:Start');
     if (m.json.buildApp)
@@ -368,16 +329,17 @@ addMessageListener(
 
     addMessageListener('AccessFu:MoveToPoint', moveToPoint);
     addMessageListener('AccessFu:MoveCursor', moveCursor);
     addMessageListener('AccessFu:ShowCurrent', showCurrent);
     addMessageListener('AccessFu:Activate', activateCurrent);
     addMessageListener('AccessFu:ContextMenu', activateContextMenu);
     addMessageListener('AccessFu:Scroll', scroll);
     addMessageListener('AccessFu:MoveCaret', moveCaret);
+    addMessageListener('AccessFu:MoveByGranularity', moveByGranularity);
 
     if (!eventManager) {
       eventManager = new EventManager(this);
     }
     eventManager.start();
   });
 
 addMessageListener(
@@ -387,13 +349,14 @@ addMessageListener(
 
     removeMessageListener('AccessFu:MoveToPoint', moveToPoint);
     removeMessageListener('AccessFu:MoveCursor', moveCursor);
     removeMessageListener('AccessFu:ShowCurrent', showCurrent);
     removeMessageListener('AccessFu:Activate', activateCurrent);
     removeMessageListener('AccessFu:ContextMenu', activateContextMenu);
     removeMessageListener('AccessFu:Scroll', scroll);
     removeMessageListener('AccessFu:MoveCaret', moveCaret);
+    removeMessageListener('AccessFu:MoveByGranularity', moveByGranularity);
 
     eventManager.stop();
   });
 
 sendAsyncMessage('AccessFu:Ready');
--- a/accessible/src/windows/msaa/AccessibleWrap.cpp
+++ b/accessible/src/windows/msaa/AccessibleWrap.cpp
@@ -1821,34 +1821,32 @@ AccessibleWrap::GetXPAccessibleFor(const
   // If lVal negative then it is treated as child ID and we should look for
   // accessible through whole accessible subtree including subdocuments.
   // Otherwise we treat lVal as index in parent.
 
   if (aVarChild.lVal < 0) {
     // Convert child ID to unique ID.
     void* uniqueID = reinterpret_cast<void*>(-aVarChild.lVal);
 
-    // Document.
+    DocAccessible* document = Document();
+    Accessible* child =
+      document->GetAccessibleByUniqueIDInSubtree(uniqueID);
+
+    // If it is a document then just return an accessible.
     if (IsDoc())
-      return AsDoc()->GetAccessibleByUniqueIDInSubtree(uniqueID);
+      return child;
 
-    // ARIA document and menu popups.
-    if (ARIARole() == roles::DOCUMENT || IsMenuPopup()) {
-      DocAccessible* document = Document();
-      Accessible* child =
-        document->GetAccessibleByUniqueIDInSubtree(uniqueID);
+    // Otherwise check whether the accessible is a child (this path works for
+    // ARIA documents and popups).
+    Accessible* parent = child;
+    while (parent && parent != document) {
+      if (parent == this)
+        return child;
 
-      // Check whether the accessible for the given ID is a child.
-      Accessible* parent = child ? child->Parent() : nullptr;
-      while (parent && parent != document) {
-        if (parent == this)
-          return child;
-
-        parent = parent->Parent();
-      }
+      parent = parent->Parent();
     }
 
     return nullptr;
   }
 
   // Gecko child indices are 0-based in contrast to indices used in MSAA.
   return GetChildAt(aVarChild.lVal - 1);
 }
--- a/accessible/src/windows/sdn/sdnTextAccessible.cpp
+++ b/accessible/src/windows/sdn/sdnTextAccessible.cpp
@@ -29,17 +29,17 @@ IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mAcc
 
 STDMETHODIMP
 sdnTextAccessible::get_domText(BSTR __RPC_FAR* aText)
 {
   A11Y_TRYBLOCK_BEGIN
 
   if (!aText)
     return E_INVALIDARG;
-  *aText = NULL;
+  *aText = nullptr;
 
   if (mAccessible->IsDefunct())
     return CO_E_OBJNOTCONNECTED;
 
   nsAutoString nodeValue;
 
   nsCOMPtr<nsIDOMNode> DOMNode(do_QueryInterface(mAccessible->GetContent()));
   DOMNode->GetNodeValue(nodeValue);
@@ -165,17 +165,17 @@ sdnTextAccessible::scrollToSubstring(uns
 
 STDMETHODIMP
 sdnTextAccessible::get_fontFamily(BSTR __RPC_FAR* aFontFamily)
 {
   A11Y_TRYBLOCK_BEGIN
 
   if (!aFontFamily)
     return E_INVALIDARG;
-  *aFontFamily = NULL;
+  *aFontFamily = nullptr;
 
   if (mAccessible->IsDefunct())
     return CO_E_OBJNOTCONNECTED;
 
   nsIFrame* frame = mAccessible->GetFrame();
   if (!frame)
     return E_FAIL;
 
--- a/accessible/tests/mochitest/attributes/test_obj.html
+++ b/accessible/tests/mochitest/attributes/test_obj.html
@@ -55,42 +55,42 @@ https://bugzilla.mozilla.org/show_bug.cg
       // ARIA
       testAttrs("live", {"live" : "polite"}, true);
       testAttrs("live2", {"live" : "polite"}, true);
       testAbsentAttrs("live3", {"live" : ""});
       testAttrs("log", {"live" : "polite"}, true);
       testAttrs("logAssertive", {"live" : "assertive"}, true);
       testAttrs("marquee", {"live" : "off"}, true);
       testAttrs("status", {"live" : "polite"}, true);
-      testAttrs("tablist", {"live" : "polite"}, true);
       testAttrs("timer", {"live" : "off"}, true);
+      testAbsentAttrs("tablist", {"live" : "polite"});
 
       // container-live object attribute
       testAttrs("liveChild", {"container-live" : "polite"}, true);
       testAttrs("live2Child", {"container-live" : "polite"}, true);
       testAttrs("logChild", {"container-live" : "polite"}, true);
       testAttrs("logAssertiveChild", {"container-live" : "assertive"}, true);
       testAttrs("marqueeChild", {"container-live" : "off"}, true);
       testAttrs("statusChild", {"container-live" : "polite"}, true);
-      testAttrs("tablistChild", {"container-live" : "polite"}, true);
       testAttrs("timerChild", {"container-live" : "off"}, true);
+      testAbsentAttrs("tablistChild", {"container-live" : "polite"});
 
       // container-live-role object attribute
       testAttrs("log", {"container-live-role" : "log"}, true);
       testAttrs("logAssertive", {"container-live-role" : "log"}, true);
       testAttrs("marquee", {"container-live-role" : "marquee"}, true);
       testAttrs("status", {"container-live-role" : "status"}, true);
       testAttrs("timer", {"container-live-role" : "timer"}, true);
       testAttrs("logChild", {"container-live-role" : "log"}, true);
       testAttrs("logAssertive", {"container-live-role" : "log"}, true);
       testAttrs("logAssertiveChild", {"container-live-role" : "log"}, true);
       testAttrs("marqueeChild", {"container-live-role" : "marquee"}, true);
       testAttrs("statusChild", {"container-live-role" : "status"}, true);
-      testAttrs("tablistChild", {"container-live-role" : "tablist"}, true);
       testAttrs("timerChild", {"container-live-role" : "timer"}, true);
+      testAbsentAttrs("tablistChild", {"container-live-role" : "tablist"});
 
       // absent aria-label and aria-labelledby object attribute
       testAbsentAttrs("label", {"label" : "foo"});
       testAbsentAttrs("labelledby", {"labelledby" : "label"});
 
       // container that has no default live attribute
       testAttrs("liveGroup", {"live" : "polite"}, true);
       testAttrs("liveGroupChild", {"container-live" : "polite"}, true);
@@ -148,19 +148,19 @@ https://bugzilla.mozilla.org/show_bug.cg
     Mozilla Bug 475006
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=558036"
      title="make HTML <output> accessible">
     Mozilla Bug 558036
   </a>
   <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=663136"
-     title="Add test coverage for tablist as implicit live region">
-    Mozilla Bug 663136
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=896400"
+     title="Tablist should no longer be an implicit live region">
+    Mozilla Bug 896400
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=563862"
      title="Expand support for nsIAccessibleEvent::OBJECT_ATTRIBUTE_CHANGE">
     Mozilla Bug 563862
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=819303"
--- a/accessible/tests/mochitest/common.js
+++ b/accessible/tests/mochitest/common.js
@@ -153,28 +153,28 @@ function isObject(aObj, aExpectedObj, aM
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Helpers for getting DOM node/accessible
 
 /**
  * Return the DOM node by identifier (may be accessible, DOM node or ID).
  */
-function getNode(aAccOrNodeOrID)
+function getNode(aAccOrNodeOrID, aDocument)
 {
   if (!aAccOrNodeOrID)
     return null;
 
   if (aAccOrNodeOrID instanceof nsIDOMNode)
     return aAccOrNodeOrID;
 
   if (aAccOrNodeOrID instanceof nsIAccessible)
     return aAccOrNodeOrID.DOMNode;
 
-  node = document.getElementById(aAccOrNodeOrID);
+  node = (aDocument || document).getElementById(aAccOrNodeOrID);
   if (!node) {
     ok(false, "Can't get DOM element for " + aAccOrNodeOrID);
     return null;
   }
 
   return node;
 }
 
--- a/accessible/tests/mochitest/events.js
+++ b/accessible/tests/mochitest/events.js
@@ -1433,16 +1433,39 @@ function moveToLineEnd(aID, aCaretOffset
 
   this.getID = function moveToLineEnd_getID()
   {
     return "move to line end in " + prettyName(aID);
   }
 }
 
 /**
+ * Move the caret to the end of previous line if any.
+ */
+function moveToPrevLineEnd(aID, aCaretOffset)
+{
+  this.__proto__ = new synthAction(aID, new caretMoveChecker(aCaretOffset, aID));
+
+  this.invoke = function moveToPrevLineEnd_invoke()
+  {
+    synthesizeKey("VK_UP", { });
+
+    if (MAC)
+      synthesizeKey("VK_RIGHT", { metaKey: true });
+    else
+      synthesizeKey("VK_END", { });
+  }
+
+  this.getID = function moveToPrevLineEnd_getID()
+  {
+    return "move to previous line end in " + prettyName(aID);
+  }
+}
+
+/**
  * Move the caret to begining of the line.
  */
 function moveToLineStart(aID, aCaretOffset)
 {
   if (MAC) {
     this.__proto__ = new synthKey(aID, "VK_LEFT", { metaKey: true },
                                   new caretMoveChecker(aCaretOffset, aID));
   } else {
--- a/accessible/tests/mochitest/jsat/test_utterance_order.html
+++ b/accessible/tests/mochitest/jsat/test_utterance_order.html
@@ -19,16 +19,33 @@ https://bugzilla.mozilla.org/show_bug.cg
       function doTest() {
         // Test the following accOrElmOrID (with optional old accOrElmOrID).
         // Note: each accOrElmOrID entry maps to a unique object utterance
         // generator function within the UtteranceGenerator.
         var tests = [{
           accOrElmOrID: "anchor",
           expected: [["link", "title"], ["title", "link"]]
         }, {
+          accOrElmOrID: "anchor_titleandtext",
+          expected: [[
+            "link", "goes to the tests -", "Tests"
+          ], [
+            "Tests", "- goes to the tests", "link"
+          ]]
+        }, {
+          accOrElmOrID: "anchor_duplicatedtitleandtext",
+          expected: [["link", "Tests"], ["Tests", "link"]]
+        }, {
+          accOrElmOrID: "anchor_arialabelandtext",
+          expected: [[
+            "link", "goes to the tests - Tests"
+          ], [
+            "Tests - goes to the tests", "link"
+          ]]
+        }, {
           accOrElmOrID: "textarea",
           expected: [[
             "text area", "This is the text area text."
           ], [
             "This is the text area text.", "text area"
           ]]
         }, {
           accOrElmOrID: "heading",
@@ -137,20 +154,27 @@ https://bugzilla.mozilla.org/show_bug.cg
     </script>
   </head>
   <body>
     <div id="root">
       <a target="_blank"
          href="https://bugzilla.mozilla.org/show_bug.cgi?id=753984"
          title="[AccessFu] utterance order test">
          Mozilla Bug 753984</a>
+      <a target="_blank"
+         href="https://bugzilla.mozilla.org/show_bug.cgi?id=758675"
+         title="[AccessFu] Add support for accDescription">
+         Mozilla Bug 758675</a>
       <p id="display"></p>
       <div id="content" style="display: none"></div>
       <pre id="test"></pre>
       <a id="anchor" href="#test" title="title"></a>
+      <a id="anchor_titleandtext" href="#test" title="goes to the tests">Tests</a>
+      <a id="anchor_duplicatedtitleandtext" href="#test" title="Tests">Tests</a>
+      <a id="anchor_arialabelandtext" href="#test" aria-label="Tests" title="goes to the tests">Tests</a>
       <textarea id="textarea" cols="80" rows="5">
         This is the text area text.
       </textarea>
       <h1 id="heading" title="Test heading"></h1>
       <ol id="list">
         <li id="li_one">list one</li>
       </ol>
       <dl id="dlist">
--- a/accessible/tests/mochitest/pivot.js
+++ b/accessible/tests/mochitest/pivot.js
@@ -3,16 +3,18 @@ Components.utils.import("resource://gre/
 ////////////////////////////////////////////////////////////////////////////////
 // Constants
 
 const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE;
 const PREFILTER_ARIA_HIDDEN = nsIAccessibleTraversalRule.PREFILTER_ARIA_HIDDEN;
 const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH;
 const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE;
 const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
+const CHAR_BOUNDARY = nsIAccessiblePivot.CHAR_BOUNDARY;
+const WORD_BOUNDARY = nsIAccessiblePivot.WORD_BOUNDARY;
 
 const NS_ERROR_NOT_IN_TREE = 0x80780026;
 const NS_ERROR_INVALID_ARG = 0x80070057;
 
 ////////////////////////////////////////////////////////////////////////////////
 // Traversal rules
 
 /**
@@ -150,16 +152,18 @@ VCChangedChecker.getPreviousPosAndOffset
 };
 
 VCChangedChecker.methodReasonMap = {
   'moveNext': nsIAccessiblePivot.REASON_NEXT,
   'movePrevious': nsIAccessiblePivot.REASON_PREV,
   'moveFirst': nsIAccessiblePivot.REASON_FIRST,
   'moveLast': nsIAccessiblePivot.REASON_LAST,
   'setTextRange': nsIAccessiblePivot.REASON_TEXT,
+  'moveNextByText': nsIAccessiblePivot.REASON_TEXT,
+  'movePreviousByText': nsIAccessiblePivot.REASON_TEXT,
   'moveToPoint': nsIAccessiblePivot.REASON_POINT
 };
 
 /**
  * Set a text range in the pivot and wait for virtual cursor change event.
  *
  * @param aDocAcc         [in] document that manages the virtual cursor
  * @param aTextAccessible [in] accessible to set to virtual cursor's position
@@ -229,16 +233,59 @@ function setVCPosInvoker(aDocAcc, aPivot
     this.eventSeq = [];
     this.unexpectedEventSeq = [
       new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc)
     ];
   }
 }
 
 /**
+ * Move the pivot by text and wait for virtual cursor change event.
+ *
+ * @param aDocAcc          [in] document that manages the virtual cursor
+ * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.)
+ * @param aBoundary        [in] boundary constant
+ * @param aTextOffsets     [in] start and end offsets of text range to set in
+ *                         virtual cursor.
+ * @param aIdOrNameOrAcc   [in] id, accessible or accessible name to expect
+ *                         virtual cursor to land on after performing move method.
+ *                         false if no move is expected.
+ */
+function setVCTextInvoker(aDocAcc, aPivotMoveMethod, aBoundary, aTextOffsets, aIdOrNameOrAcc)
+{
+  var expectMove = (aIdOrNameOrAcc != false);
+  this.invoke = function virtualCursorChangedInvoker_invoke()
+  {
+    VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor);
+    SimpleTest.info(aDocAcc.virtualCursor.position);
+    var moved = aDocAcc.virtualCursor[aPivotMoveMethod](aBoundary);
+    SimpleTest.is(!!moved, !!expectMove,
+                  "moved pivot by text with " + aPivotMoveMethod +
+                  " to " + aIdOrNameOrAcc);
+  };
+
+  this.getID = function setVCPosInvoker_getID()
+  {
+    return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod;
+  };
+
+  if (expectMove) {
+    this.eventSeq = [
+      new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod)
+    ];
+  } else {
+    this.eventSeq = [];
+    this.unexpectedEventSeq = [
+      new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc)
+    ];
+  }
+}
+
+
+/**
  * Move the pivot to the position under the point.
  *
  * @param aDocAcc        [in] document that manages the virtual cursor
  * @param aX             [in] screen x coordinate
  * @param aY             [in] screen y coordinate
  * @param aIgnoreNoMatch [in] don't unset position if no object was found at
  *                       point.
  * @param aRule          [in] traversal rule object
--- a/accessible/tests/mochitest/pivot/Makefile.in
+++ b/accessible/tests/mochitest/pivot/Makefile.in
@@ -8,12 +8,14 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_A11Y_FILES = \
 		doc_virtualcursor.html \
+		doc_virtualcursor_text.html \
 		test_virtualcursor.html \
+		test_virtualcursor_text.html \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Pivot test document</title>
+  <meta charset="utf-8" />
+</head>
+<body>
+  <p id="paragraph-1">
+  This <b>is</b> <a href="#">the</a> test of text.
+  </p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/pivot/test_virtualcursor_text.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Tests pivot functionality in virtual cursors</title>
+  <meta charset="utf-8" />
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js">
+  </script>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/chrome-harness.js">
+  </script>
+
+  <script type="application/javascript" src="../common.js"></script>
+  <script type="application/javascript" src="../browser.js"></script>
+  <script type="application/javascript" src="../events.js"></script>
+  <script type="application/javascript" src="../role.js"></script>
+  <script type="application/javascript" src="../states.js"></script>
+  <script type="application/javascript" src="../pivot.js"></script>
+  <script type="application/javascript" src="../layout.js"></script>
+
+  <script type="application/javascript">
+    var gBrowserWnd = null;
+    var gQueue = null;
+
+    function doTest()
+    {
+      var doc = currentTabDocument();
+      var docAcc = getAccessible(doc, [nsIAccessibleDocument]);
+
+      gQueue = new eventQueue();
+
+      gQueue.onFinish = function onFinish()
+      {
+        closeBrowserWindow();
+      }
+
+      gQueue.push(new setVCPosInvoker(docAcc, null, null,
+                                      getAccessible(doc.getElementById('paragraph-1'))));
+
+      gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [0,4],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', CHAR_BOUNDARY, [4,5],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'movePreviousByText', CHAR_BOUNDARY, [3,4],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [5,7],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [8,9],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [10,14],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'movePreviousByText', WORD_BOUNDARY, [8,9],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+      gQueue.push(new setVCTextInvoker(docAcc, 'movePreviousByText', WORD_BOUNDARY, [5,7],
+                  getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText)));
+
+      gQueue.invoke();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addLoadEvent(function () {
+      /* We open a new browser because we need to test with a top-level content
+         document. */
+      openBrowserWindow(
+        doTest,
+        getRootDirectory(window.location.href) + "doc_virtualcursor_text.html");
+    });
+  </script>
+</head>
+<body id="body">
+
+  <a target="_blank"
+     title="Support Movement By Granularity"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=886076">Mozilla Bug 886076</a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+</body>
+</html>
--- a/accessible/tests/mochitest/states/test_aria.html
+++ b/accessible/tests/mochitest/states/test_aria.html
@@ -193,16 +193,24 @@
       testStates("aria_vslider", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
 
       // indeterminate ARIA progressbars (no aria-valuenow or aria-valuetext attribute)
       // should expose mixed state
       testStates("aria_progressbar", STATE_MIXED);
       testStates("aria_progressbar_valuenow", 0, 0, STATE_MIXED);
       testStates("aria_progressbar_valuetext", 0, 0, STATE_MIXED);
 
+      testStates("aria_listbox", STATE_FOCUSABLE);
+      testStates("aria_grid", STATE_FOCUSABLE);
+      testStates("aria_tree", STATE_FOCUSABLE);
+      testStates("aria_treegrid", STATE_FOCUSABLE);
+      testStates("aria_listbox_disabled", 0, 0, STATE_FOCUSABLE);
+      testStates("aria_grid_disabled", 0, 0, STATE_FOCUSABLE);
+      testStates("aria_tree_disabled", 0, 0, STATE_FOCUSABLE);
+      testStates("aria_treegrid_disabled", 0, 0, STATE_FOCUSABLE);
       SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 
 </head>
@@ -215,60 +223,66 @@
     Mozilla Bug 457219
   </a><br />
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=429285"
      title="Propagate aria-disabled to descendants">
     Mozilla Bug 429285
   </a>
   <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=457226"
+     title="Mochitests for ARIA states">
+    Mozilla Bug 457226
+  </a>
+  <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=499653"
      title="Unify ARIA state attributes mapping rules">
     Mozilla Bug 499653
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674"
      title="aria-autocomplete not supported on standard form text input controls">
     Mozilla Bug 681674
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674"
      title="aria-orientation should be applied to separator and slider roles">
     Mozilla Bug 681674
   </a>
   <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847"
+     title="Expose active state on current item of selectable widgets">
+    Mozilla Bug 689847
+  </a>
+  <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017"
      title="File input control should be propogate states to descendants">
     Mozilla Bug 699017
   </a>
   <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847"
-     title="Expose active state on current item of selectable widgets">
-    Mozilla Bug 689847
-  </a>
-  <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=457226"
-     title="Mochitests for ARIA states">
-    Mozilla Bug 457226
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=690199"
+     title="ARIA select widget should expose focusable state regardless the way they manage its children">
+    Mozilla Bug 690199
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=740851"
      title="ARIA undetermined progressmeters should expose mixed state">
     Mozilla Bug 740851
   </a>
   <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=762876
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=762876"
      title="fix default horizontal / vertical state of role=scrollbar and ensure only one of horizontal / vertical states is exposed">
     Mozilla Bug 762876
   </a>
   <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=835121
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=835121"
      title="ARIA grid should be editable by default">
     Mozilla Bug 835121
   </a>
+
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <div id="textbox_autocomplete_inline" role="textbox" aria-autocomplete="inline"></div>
   <div id="textbox_autocomplete_list" role="textbox" aria-autocomplete="list"></div>
   <div id="textbox_autocomplete_both" role="textbox" aria-autocomplete="both"></div>
@@ -408,10 +422,44 @@
   <div id="aria_slider" role="slider">slider</div>
   <div id="aria_hslider" role="slider" aria-orientation="horizontal">horizontal slider</div>
   <div id="aria_vslider" role="slider" aria-orientation="vertical">vertical slider</div>
   
   <!-- indeterminate ARIA progressbars should expose mixed state -->
   <div id="aria_progressbar" role="progressbar"></div>
   <div id="aria_progressbar_valuenow" role="progressbar" aria-valuenow="1"></div>
   <div id="aria_progressbar_valuetext" role="progressbar" aria-valuetext="value"></div>
+
+  <!-- ARIA select widget should expose focusable state regardless the way they manage its children -->
+  <div id="aria_listbox" role="listbox">
+    <div role="option" tabindex="0">A</div>
+    <div role="option" tabindex="0">a</div>
+  </div>
+  <div id="aria_grid" role="grid">
+    <div role="row"><div role="gridcell" tabindex="0">B</div></div></div>
+    <div role="row"><div role="gridcell" tabindex="0">b</div></div></div>
+  <div id="aria_tree" role="tree">
+    <div role="treeitem" tabindex="0">C</div>
+    <div role="treeitem" tabindex="0">c</div>
+  </div>
+  <div id="aria_treegrid" role="treegrid">
+    <div role="row"><div role="gridcell" tabindex="0">D</div></div>
+    <div role="row"><div role="gridcell" tabindex="0">d</div></div>
+  </div>
+  <div id="aria_listbox_disabled" role="listbox" aria-disabled="true">
+    <div role="option">E</div>
+    <div role="option">e</div>
+  </div>
+  <div id="aria_grid_disabled" role="grid" aria-disabled="true">
+    <div role="row"><div role="gridcell">F</div></div>
+    <div role="row"><div role="gridcell">f</div></div>
+  </div>
+  <div id="aria_tree_disabled" role="tree" aria-disabled="true">
+    <div role="treeitem">G</div>
+    <div role="treeitem">g</div>
+  </div>
+  <div id="aria_treegrid_disabled" role="treegrid" aria-disabled="true">
+    <div role="row"><div role="gridcell">H</div></div>
+    <div role="row"><div role="gridcell">h</div></div>
+  </div>
+
 </body>
 </html>
--- a/accessible/tests/mochitest/text/Makefile.in
+++ b/accessible/tests/mochitest/text/Makefile.in
@@ -13,18 +13,17 @@ include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_A11Y_FILES = \
 		doc.html \
 		test_atcaretoffset.html \
 		test_charboundary.html \
 		test_doc.html \
 		test_gettext.html \
 		test_hypertext.html \
+		test_lineboundary.html \
 		test_label.xul \
-		test_multiline.html \
 		test_passwords.html \
 		test_selection.html \
-		test_singleline.html \
 		test_wordboundary.html \
 		test_words.html \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/accessible/tests/mochitest/text/test_atcaretoffset.html
+++ b/accessible/tests/mochitest/text/test_atcaretoffset.html
@@ -22,220 +22,118 @@
   <script type="application/javascript"
           src="../events.js"></script>
   <script type="application/javascript"
           src="../text.js"></script>
 
   <script type="application/javascript">
     //gA11yEventDumpToConsole = true; // debugging
 
-    // __a__w__o__r__d__\n
-    //  0  1  2  3  4  5
-    // __t__w__o__ (soft line break)
-    //  6  7  8  9
-    // __w__o__r__d__s
-    // 10 11 12 13 14 15
+    function traverseTextByLines(aQueue, aID, aLines)
+    {
+      var baseInvoker = new synthFocus(aID);
+      var baseInvokerID = "move to last line end";
 
-    function moveToLastLineEnd()
-    {
-      this.__proto__ = new synthFocus("textarea");
-
-      this.finalCheck = function moveToLastLineEnd_finalCheck()
-      {
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "", 15, 15,
-                            ["textarea"]);
+      for (var i = aLines.length - 1; i >= 0 ; i--) {
+        var [ ppLineText, ppLineEndChar, ppLineStart, ppLineEnd ] =
+          (i - 2 >= 0) ? aLines[i - 2] : [ "", "", 0, 0 ];
+        var [ pLineText, pLineEndChar, pLineStart, pLineEnd ] =
+          (i - 1 >= 0) ? aLines[i - 1] : [ "", "", 0, 0 ];
+        var [ lineText, lineEndChar, lineStart, lineEnd ] = aLines[i];
 
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "", 15, 15,
-                            [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
-                         [ "textarea" ]);
+        var [ nLineText, nLineEndChar, nLineStart, nLineEnd ] =
+          (i + 1 < aLines.length) ?
+            aLines[i + 1] :
+            [ "", "", lineEnd  + lineEndChar.length, lineEnd + lineEndChar.length ];
 
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
-                         [ "textarea" ]);
+        var [ nnLineText, nnLineEndChar, nnLineStart, nnLineEnd ] =
+          (i + 2 < aLines.length) ?
+            aLines[i + 2] :
+            [ "", "", nLineEnd  + nLineEndChar.length, nLineEnd + nLineEndChar.length ];
 
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                             [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                             "textarea", kTodo, kTodo, kOk);
-      }
+        var tests = [
+          [ testTextBeforeOffset, BOUNDARY_LINE_START,
+            pLineText + pLineEndChar, pLineStart, lineStart],
 
-      this.getID = function moveToLastLineEnd_getID()
-      {
-        return "move to last line end";
-      }
-    }
+          [ testTextBeforeOffset, BOUNDARY_LINE_END,
+            ppLineEndChar + pLineText, ppLineEnd, pLineEnd],
+
+          [ testTextAtOffset, BOUNDARY_LINE_START,
+            lineText + lineEndChar, lineStart, nLineStart],
 
-    function moveToLastLineStart()
-    {
-      this.__proto__ = new moveToLineStart("textarea", 10);
+          [ testTextAtOffset, BOUNDARY_LINE_END,
+            pLineEndChar + lineText, pLineEnd, lineEnd],
 
-      this.finalCheck = function moveToLastLineStart_finalCheck()
-      {
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "", 15, 15,
-                            [ "textarea" ]);
+          [ testTextAfterOffset, BOUNDARY_LINE_START,
+            nLineText + nnLineEndChar, nLineStart, nnLineStart],
 
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "", 15, 15,
-                            [ "textarea" ]);
+          [ testTextAfterOffset, BOUNDARY_LINE_END,
+            lineEndChar + nLineText, lineEnd, nLineEnd],
+        ];
 
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
-                         [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
-                         [ "textarea" ]);
+        aQueue.push(new tmpl_moveTo(aID, baseInvoker, baseInvokerID, tests));
 
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                             [ "textarea" ]);
+        baseInvoker = new moveToLineStart(aID, lineStart);
+        baseInvokerID = "move to " + i + "th line start";
 
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                             "textarea", kTodo, kTodo, kOk);
-      }
+        aQueue.push(new tmpl_moveTo(aID, baseInvoker, baseInvokerID, tests));
 
-      this.getID = function moveToLastLineStart_getID()
-      {
-        return "move to last line start";
+        baseInvoker = new moveToPrevLineEnd(aID, pLineEnd);
+        baseInvokerID = "move to " + (i - 1) + "th line end";
       }
     }
 
-    function moveToMiddleLineStart()
+    /**
+     * A template invoker to move through the text.
+     */
+    function tmpl_moveTo(aID, aInvoker, aInvokerID, aTests)
     {
-      this.__proto__ = new synthUpKey("textarea",
-                                      new caretMoveChecker(6, "textarea"));
-
-      this.finalCheck = function moveToMiddleLineStart_finalCheck()
-      {
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
-                            [ "textarea" ]);
-
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
-                            [ "textarea" ]);
+      this.__proto__ = aInvoker;
 
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                         [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                         [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
-                             [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
-                             [ "textarea" ]);
-      }
-
-      this.getID = function moveToMiddleLineStart_getID()
+      this.finalCheck = function genericMoveTo_finalCheck()
       {
-        return "move to middle line start";
-      }
-    }
-
-    function moveToMiddleLineEnd()
-    {
-      this.__proto__ = new moveToLineEnd("textarea", 10);
-
-      this.finalCheck = function moveToMiddleLineEnd_finalCheck()
-      {
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15,
-                            [ "textarea" ]);
-
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15,
-                            [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                         [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                         [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
-                             [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
-                             [ "textarea" ]);
+        for (var i = 0; i < aTests.length; i++) {
+          aTests[i][0].call(null, kCaretOffset, aTests[i][1],
+                            aTests[i][2], aTests[i][3], aTests[i][4], aID,
+                            kOk, kOk, kOk);
+        }
       }
 
-      this.getID = function moveToMiddleLineEnd_getID()
-      {
-        return "move to middle line end";
-      }
-    }
-
-    function moveToFirstLineStart()
-    {
-      this.__proto__ = new moveToTextStart("textarea");
-
-      this.finalCheck = function moveToFirstLineStart_finalCheck()
-      {
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                            [ "textarea" ]);
-
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                            [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
-                         [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
-                         [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "", 0, 0,
-                             [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0,
-                             "textarea", kOk, kOk, kOk);
-      }
-
-      this.getID = function moveToFirstLineStart_getID()
+      this.getID = function genericMoveTo_getID()
       {
-        return "move to first line start";
-      }
-    }
-
-    function moveToFirstLineEnd()
-    {
-      this.__proto__ = new moveToLineEnd("textarea", 5);
-
-      this.finalCheck = function moveToFirstLineStart_finalCheck()
-      {
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10,
-                            [ "textarea" ]);
-
-        testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10,
-                            [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6,
-                         [ "textarea" ]);
-
-        testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5,
-                         [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "", 0, 0,
-                             [ "textarea" ]);
-
-        testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0,
-                             [ "textarea" ]);
-      }
-
-      this.getID = function moveToFirstLineEnd_getID()
-      {
-        return "move to first line end";
+        return aInvokerID;
       }
     }
 
     var gQueue = null;
     function doTest()
     {
       gQueue = new eventQueue();
-      gQueue.push(new moveToLastLineEnd());
-      gQueue.push(new moveToLastLineStart());
-      gQueue.push(new moveToMiddleLineStart());
-      gQueue.push(new moveToMiddleLineEnd());
-      gQueue.push(new moveToFirstLineStart());
-      gQueue.push(new moveToFirstLineEnd());
+
+      // __a__w__o__r__d__\n
+      //  0  1  2  3  4  5
+      // __t__w__o__ (soft line break)
+      //  6  7  8  9
+      // __w__o__r__d__s
+      // 10 11 12 13 14 15
+
+      traverseTextByLines(gQueue, "textarea",
+                          [ [ "aword", "\n", 0, 5 ],
+                            [ "two ", "", 6, 10 ],
+                            [ "words", "", 10, 15 ]] );
+
+      traverseTextByLines(gQueue, "ta_wrapped", 
+                          [ [ "hi ", "", 0, 3 ],
+                            [ "hello", "", 3, 8 ],
+                            [ " my ", "", 8, 12 ],
+                            [ "longf", "", 12, 17 ],
+                            [ "riend", "", 17, 22 ],
+                            [ " t ", "", 22, 25 ],
+                            [ "sq t", "", 25, 29 ]] );
+
       gQueue.invoke(); // will call SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 </head>
 <body>
@@ -246,11 +144,13 @@
    Bug 852021
   </a>
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
 
   <textarea id="textarea" cols="5">aword
 two words</textarea>
+
+  <textarea id="ta_wrapped" cols="5">hi hello my longfriend t sq t</textarea>
   </pre>
 </body>
 </html>
rename from accessible/tests/mochitest/text/test_singleline.html
rename to accessible/tests/mochitest/text/test_lineboundary.html
--- a/accessible/tests/mochitest/text/test_singleline.html
+++ b/accessible/tests/mochitest/text/test_lineboundary.html
@@ -1,112 +1,156 @@
 <!DOCTYPE html>
 <html>
 <head>
-  <title>nsIAccessibleText getText related function tests for html:input,html:div and html:textarea</title>
+  <title>Line boundary getText* functions tests</title>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript"
           src="../common.js"></script>
   <script type="application/javascript"
           src="../text.js"></script>
   <script type="application/javascript">
     function doTest()
     {
+      //////////////////////////////////////////////////////////////////////////
       // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__
       //  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
 
-      ////////////////////////////////////////////////////////////////////////
-      // getTextAfterOffset
+      var IDs = [ "input", "div", "editable", "textarea",
+                  getNode("ta", getNode("ta_cntr").contentDocument) ];
+
+      testTextBeforeOffset(IDs, BOUNDARY_LINE_START,
+                           [ [ 0, 15, "", 0, 0 ] ]);
+      testTextBeforeOffset(IDs, BOUNDARY_LINE_END,
+                           [ [ 0, 15, "", 0, 0 ] ]);
 
-      var IDs = [ "input", "div", "editable", "textarea" ];
-      var regularIDs = [ "input", "div", "editable" ];
+      testTextAtOffset(IDs, BOUNDARY_LINE_START,
+                       [ [ 0, 15, "hello my friend", 0, 15 ] ]);
+      testTextAtOffset(IDs, BOUNDARY_LINE_END,
+                       [ [ 0, 15, "hello my friend", 0, 15 ] ]);
+
+      testTextAfterOffset(IDs, BOUNDARY_LINE_START,
+                          [ [ 0, 15, "", 15, 15 ] ]);
+      testTextAfterOffset(IDs, BOUNDARY_LINE_END,
+                          [ [ 0, 15, "", 15, 15 ] ]);
 
-      // BOUNDARY_LINE_START
-      testTextAfterOffset(0, BOUNDARY_LINE_START, "", 15, 15, IDs);
-      testTextAfterOffset(1, BOUNDARY_LINE_START, "", 15, 15, IDs);
-      testTextAfterOffset(14, BOUNDARY_LINE_START, "", 15, 15, IDs);
-      testTextAfterOffset(15, BOUNDARY_LINE_START, "", 15, 15, IDs);
+      //////////////////////////////////////////////////////////////////////////
+      // __o__n__e__w__o__r__d__\n
+      //  0  1  2  3  4  5  6  7
+      // __\n
+      //  8
+      // __t__w__o__ __w__o__r__d__s__\n
+      //  9 10 11 12 13 14 15 16 17 18
+
+      IDs = [ "ml_div", "ml_divbr", "ml_editable", "ml_editablebr", "ml_textarea"];
 
-      // BOUNDARY_LINE_END
-      testTextAfterOffset(0, BOUNDARY_LINE_END, "", 15, 15, IDs);
-      testTextAfterOffset(1, BOUNDARY_LINE_END, "", 15, 15, IDs);
-      testTextAfterOffset(14, BOUNDARY_LINE_END, "", 15, 15, IDs);
-      testTextAfterOffset(15, BOUNDARY_LINE_END, "", 15, 15, IDs);
-
-      ////////////////////////////////////////////////////////////////////////
-      // getTextBeforeOffset
-
-      var IDs = [ "input", "div", "editable", "textarea" ];
+      testTextBeforeOffset(IDs, BOUNDARY_LINE_START,
+                           [ [ 0, 7, "", 0, 0 ],
+                             [ 8, 8, "oneword\n", 0, 8 ],
+                             [ 9, 18, "\n", 8, 9 ],
+                             [ 19, 19, "two words\n", 9, 19 ]]);
+      testTextBeforeOffset(IDs, BOUNDARY_LINE_END,
+                          [ [ 0, 7, "", 0, 0 ],
+                            [ 8, 8, "oneword", 0, 7 ],
+                            [ 9, 18, "\n", 7, 8 ],
+                            [ 19, 19, "\ntwo words", 8, 18 ]]);
 
-      // BOUNDARY_LINE_START
-      testTextBeforeOffset(0, BOUNDARY_LINE_START, "", 0, 0, IDs);
-      testTextBeforeOffset(1, BOUNDARY_LINE_START, "", 0, 0, IDs);
-      testTextBeforeOffset(14, BOUNDARY_LINE_START, "", 0, 0, IDs);
-      testTextBeforeOffset(15, BOUNDARY_LINE_START, "", 0, 0, IDs);
+      testTextAtOffset(IDs, BOUNDARY_LINE_START,
+                       [ [ 0, 7, "oneword\n", 0, 8 ],
+                         [ 8, 8, "\n", 8, 9 ],
+                         [ 9, 18, "two words\n", 9, 19 ],
+                         [ 19, 19, "", 19, 19 ]]);
+      testTextAtOffset(IDs, BOUNDARY_LINE_END,
+                       [ [ 0, 7, "oneword", 0, 7 ],
+                         [ 8, 8, "\n", 7, 8 ],
+                         [ 9, 18, "\ntwo words", 8, 18 ],
+                         [ 19, 19, "\n", 18, 19 ]]);
 
-      // BOUNDARY_LINE_END
-      testTextBeforeOffset(0, BOUNDARY_LINE_END, "", 0, 0, IDs);
-      testTextBeforeOffset(1, BOUNDARY_LINE_END, "", 0, 0, IDs);
-      testTextBeforeOffset(14, BOUNDARY_LINE_END, "", 0, 0, IDs);
-      testTextBeforeOffset(15, BOUNDARY_LINE_END, "", 0, 0, IDs);
-
-      ////////////////////////////////////////////////////////////////////////
-      // getTextAtOffset
+      testTextAfterOffset(IDs, BOUNDARY_LINE_START,
+                          [ [ 0, 7, "\n", 8, 9 ],
+                            [ 8, 8, "two words\n", 9, 19 ],
+                            [ 9, 19, "", 19, 19 ]]);
+      testTextAfterOffset(IDs, BOUNDARY_LINE_END,
+                          [ [ 0, 7, "\n", 7, 8 ],
+                            [ 8, 8, "\ntwo words", 8, 18 ],
+                            [ 9, 18, "\n", 18, 19 ],
+                            [ 19, 19, "", 19, 19 ]]);
 
-      IDs = [ "input", "div", "editable", "textarea" ];
-      regularIDs = [ "input", "div", "editable" ];
+      //////////////////////////////////////////////////////////////////////////
+      // a * b (* is embedded char for link)
+      testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START,
+                           [ [ 0, 5, "", 0, 0 ] ]);
+
+      testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END,
+                           [ [ 0, 5, "", 0, 0 ] ]);
 
-      // BOUNDARY_LINE_START
-      testTextAtOffset(0, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs);
-      testTextAtOffset(1, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs);
-      testTextAtOffset(14, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs);
-      testTextAtOffset(15, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs);
+      testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START,
+                       [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]);
+
+      testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END,
+                       [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]);
 
-      // BOUNDARY_LINE_END
-      testTextAtOffset(0, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs);
-      testTextAtOffset(1, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs);
-      testTextAtOffset(14, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs);
-      testTextAtOffset(15, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs);
+      testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START,
+                          [ [ 0, 5, "", 5, 5 ] ]);
 
+      testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END,
+                          [ [ 0, 5, "", 5, 5 ] ]);
+                           
       SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 </head>
 <body>
 
   <a target="_blank"
-     title="nsIAccessibleText getText related function tests for html:input,html:div and html:textarea"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=452769">
-    Bug 452769
-  </a>
-  <a target="_blank"
      title="getTextAtOffset for word boundaries: beginning of a new life"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=853340">
     Bug 853340
   </a>
   <a target="_blank"
      title="getTextBeforeOffset for word boundaries: evolving"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=855732">
     Bug 855732
   </a>
   <a target="_blank"
      title=" getTextAfterOffset for line boundary on new rails"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=882292">
     Bug 882292
   </a>
+  
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <input id="input" value="hello my friend"/>
   <div id="div">hello my friend</div>
   <div id="editable" contenteditable="true">hello my friend</div>
   <textarea id="textarea">hello my friend</textarea>
+  <iframe id="ta_cntr"
+          src="data:text/html,<html><body><textarea id='ta'>hello my friend</textarea></body></html>"></iframe>
 
+  <pre>
+    <div id="ml_div">oneword
+
+two words
+</div>
+  <div id="ml_divbr">oneword<br/><br/>two words<br/></div>
+  <div id="ml_editable" contenteditable="true">oneword
+
+two words
+</div>
+  <div id="ml_editablebr" contenteditable="true">oneword<br/><br/>two words<br/></div>
+  <textarea id="ml_textarea" cols="300">oneword
+
+two words
+</textarea>
+  </pre>
+
+  <iframe id="ht_1" src="data:text/html,<html><body>a <a href=''>b</a> c</body></html>"></iframe>
 </body>
 </html>
deleted file mode 100644
--- a/accessible/tests/mochitest/text/test_multiline.html
+++ /dev/null
@@ -1,137 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <title>nsIAccessibleText getText related function in multiline text</title>
-  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
-
-  <script type="application/javascript"
-          src="chrome://mochikit/content/MochiKit/packed.js"></script>
-  <script type="application/javascript"
-          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript"
-          src="../common.js"></script>
-  <script type="application/javascript"
-          src="../text.js"></script>
-  <script type="application/javascript">
-
-    function doTest()
-    {
-      // __o__n__e__w__o__r__d__\n
-      //  0  1  2  3  4  5  6  7
-      // __\n
-      //  8
-      // __t__w__o__ __w__o__r__d__s__\n
-      //  9 10 11 12 13 14 15 16 17 18
-
-      ////////////////////////////////////////////////////////////////////////
-      // getText
-
-      var IDs = ["div", "divbr", "editable", "editablebr", "textarea"];
-
-      ////////////////////////////////////////////////////////////////////////
-      // getTextAfterOffset
-
-      // BOUNDARY_LINE_START
-      testTextAfterOffset(0, BOUNDARY_LINE_START, "\n", 8, 9, IDs);
-      testTextAfterOffset(7, BOUNDARY_LINE_START, "\n", 8, 9, IDs);
-      testTextAfterOffset(8, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
-      testTextAfterOffset(9, BOUNDARY_LINE_START, "", 19, 19, IDs);
-      testTextAfterOffset(19, BOUNDARY_LINE_START, "", 19, 19, IDs);
-
-      // BOUNDARY_LINE_END
-      testTextAfterOffset(0, BOUNDARY_LINE_END, "\n", 7, 8, IDs);
-      testTextAfterOffset(7, BOUNDARY_LINE_END, "\n", 7, 8, IDs);
-      testTextAfterOffset(8, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
-      testTextAfterOffset(9, BOUNDARY_LINE_END, "\n", 18, 19, IDs);
-      testTextAfterOffset(18, BOUNDARY_LINE_END, "\n", 18, 19, IDs);
-      testTextAfterOffset(19, BOUNDARY_LINE_END, "", 19, 19, IDs);
-
-      ////////////////////////////////////////////////////////////////////////
-      // getTextBeforeOffset
-
-      // BOUNDARY_LINE_START
-      testTextBeforeOffset(0, BOUNDARY_LINE_START, "", 0, 0, IDs);
-      testTextBeforeOffset(8, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs);
-      testTextBeforeOffset(9, BOUNDARY_LINE_START, "\n", 8, 9, IDs);
-      testTextBeforeOffset(18, BOUNDARY_LINE_START, "\n", 8, 9, IDs);
-      testTextBeforeOffset(19, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
-
-      // BOUNDARY_LINE_END
-      testTextBeforeOffset(0, BOUNDARY_LINE_END, "", 0, 0, IDs);
-      testTextBeforeOffset(7, BOUNDARY_LINE_END, "", 0, 0, IDs);
-      testTextBeforeOffset(8, BOUNDARY_LINE_END, "oneword", 0, 7, IDs);
-      testTextBeforeOffset(9, BOUNDARY_LINE_END, "\n", 7, 8, IDs);
-      testTextBeforeOffset(18, BOUNDARY_LINE_END, "\n", 7, 8, IDs);
-      testTextBeforeOffset(19, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
-
-      ////////////////////////////////////////////////////////////////////////
-      // getTextAtOffset
-
-      // BOUNDARY_LINE_START
-      testTextAtOffset(0, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs);
-      testTextAtOffset(7, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs);
-      testTextAtOffset(8, BOUNDARY_LINE_START, "\n", 8, 9, IDs);
-      testTextAtOffset(9, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
-      testTextAtOffset(13, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
-      testTextAtOffset(18, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs);
-      testTextAtOffset(19, BOUNDARY_LINE_START, "", 19, 19, IDs);
-
-      // BOUNDARY_LINE_END
-      testTextAtOffset(0, BOUNDARY_LINE_END, "oneword", 0, 7, IDs);
-      testTextAtOffset(7, BOUNDARY_LINE_END, "oneword", 0, 7, IDs);
-      testTextAtOffset(8, BOUNDARY_LINE_END, "\n", 7, 8, IDs);
-      testTextAtOffset(9, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
-      testTextAtOffset(17, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
-      testTextAtOffset(18, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs);
-      testTextAtOffset(19, BOUNDARY_LINE_END, "\n", 18, 19, IDs);
-
-      SimpleTest.finish();
-    }
-
-    SimpleTest.waitForExplicitFinish();
-    addA11yLoadEvent(doTest);
-  </script>
-</head>
-<body>
-
-  <a target="_blank"
-     title="nsIAccessibleText getText related functions test in multiline text"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=612331">
-   Bug 612331
-  </a>
-  <a target="_blank"
-     title="getTextAtOffset for word boundaries: beginning of a new life"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=853340">
-    Bug 853340
-  </a>
-  <a target="_blank"
-     title="getTextBeforeOffset for word boundaries: evolving"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=855732">
-    Bug 855732
-  </a>
-  <a target="_blank"
-     title=" getTextAfterOffset for line boundary on new rails"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=882292">
-    Bug 882292
-  </a>
-  <p id="display"></p>
-  <div id="content" style="display: none"></div>
-  <pre id="test">
-
-  <div id="div">oneword
-
-two words
-</div>
-  <div id="divbr">oneword<br/><br/>two words<br/></div>
-  <div id="editable" contenteditable="true">oneword
-
-two words
-</div>
-  <div id="editablebr" contenteditable="true">oneword<br/><br/>two words<br/></div>
-  <textarea id="textarea" cols="300">oneword
-
-two words
-</textarea>
-  </pre>
-</body>
-</html>
--- a/accessible/tests/mochitest/value/test_general.html
+++ b/accessible/tests/mochitest/value/test_general.html
@@ -46,16 +46,21 @@
       testValue("aria_application_link", "");
 
       // roles that can live as HTMLLinkAccessibles
       testValue("aria_link_link", href);
       testValue("aria_main_link", href);
       testValue("aria_navigation_link", href);
 
       //////////////////////////////////////////////////////////////////////////
+      // ARIA textboxes
+
+      testValue("aria_textbox1", "helo");
+
+      //////////////////////////////////////////////////////////////////////////
       // ARIA comboboxes
 
       // aria-activedescendant defines a current item the value is computed from
       testValue("aria_combobox1", kDiscBulletText + "Zoom");
 
       // aria-selected defines a selected item the value is computed from,
       // list control is pointed by aria-owns relation.
       testValue("aria_combobox2", kDiscBulletText + "Zoom");
@@ -78,22 +83,27 @@
 
 </head>
 
 <body>
 
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=494807"
      title="Do not expose a11y info specific to hyperlinks when role is overridden using ARIA">
-    Mozilla Bug 494807
+    Bug 494807
   </a>
   <a target="_blank"
      href="https://bugzilla.mozilla.org/show_bug.cgi?id=819273"
-     title=" ARIA combobox should have accessible value">
-    Mozilla Bug 819273
+     title="ARIA combobox should have accessible value">
+    Bug 819273
+  </a>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=887250"
+     title="ARIA textbox role doesn't expose value">
+    Bug 887250
   </a>
   <p id="display"></p>
   <div id="content" style="display: none">
   </div>
   <pre id="test">
   </pre>
 
   <a id="aria_menuitem_link" role="menuitem" href="foo">menuitem</a>
@@ -103,16 +113,18 @@
   <!-- landmark links -->
   <a id="aria_application_link" role="application" href="foo">app</a>
   <a id="aria_main_link" role="main" href="foo">main</a>
   <a id="aria_navigation_link" role="navigation" href="foo">nav</a>
 
   <!-- strange edge case: please don't do this in the wild -->
   <a id="aria_link_link" role="link" href="foo">link</a>
 
+  <div id="aria_textbox1" role="textbox">helo</div>
+
   <div id="aria_combobox1" role="combobox"
        aria-owns="aria_combobox1_owned_listbox"
        aria-activedescendant="aria_combobox1_selected_option">
   </div>
   <ul role="listbox" id="aria_combobox1_owned_listbox">
     <li role="option">Zebra</li>
     <li role="option" id="aria_combobox1_selected_option">Zoom</li>
   </ul>
@@ -137,10 +149,11 @@
   <select id="combobox1">
     <option id="cb1_item1">item1</option>
     <option id="cb1_item2">item2</option>
   </select>
   <select id="combobox2">
     <option id="cb2_item1">item1</option>
     <option id="cb2_item2" selected="true">item2</option>
   </select>
+
 </body>
 </html>
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -172,16 +172,18 @@ pref("geo.enabled", true);
 // see https://bugzilla.mozilla.org/show_bug.cgi?id=481566#c9
 pref("content.sink.enable_perf_mode",  2); // 0 - switch, 1 - interactive, 2 - perf
 pref("content.sink.pending_event_mode", 0);
 pref("content.sink.perf_deflect_count", 1000000);
 pref("content.sink.perf_parse_time", 50000000);
 
 // Maximum scripts runtime before showing an alert
 pref("dom.max_chrome_script_run_time", 0); // disable slow script dialog for chrome
+// Disable the watchdog thread for B2G. See bug 870043 comment 31.
+pref("dom.use_watchdog", false);
 
 // plugins
 pref("plugin.disable", true);
 pref("dom.ipc.plugins.enabled", true);
 
 // product URLs
 // The breakpad report server to link to in about:crashes
 pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -193,17 +193,21 @@ Components.utils.import('resource://gre/
   lock.set('deviceinfo.product_model', product_model, null, null);
 })();
 
 // =================== Debugger ====================
 SettingsListener.observe('devtools.debugger.remote-enabled', false, function(value) {
   Services.prefs.setBoolPref('devtools.debugger.remote-enabled', value);
   // This preference is consulted during startup
   Services.prefs.savePrefFile(null);
-  value ? RemoteDebugger.start() : RemoteDebugger.stop();
+  try {
+    value ? RemoteDebugger.start() : RemoteDebugger.stop();
+  } catch(e) {
+    dump("Error while initializing devtools: " + e + "\n" + e.stack + "\n");
+  }
 
 #ifdef MOZ_WIDGET_GONK
   let enableAdb = value;
 
   try {
     if (Services.prefs.getBoolPref('marionette.defaultPrefs.enabled')) {
       // Marionette is enabled. Force adb on, since marionette requires remote
       // debugging to be disabled (we don't want adb to track the remote debugger
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -551,17 +551,18 @@ var shell = {
   openAppForSystemMessage: function shell_openAppForSystemMessage(msg) {
     let origin = Services.io.newURI(msg.manifest, null, null).prePath;
     this.sendChromeEvent({
       type: 'open-app',
       url: msg.uri,
       manifestURL: msg.manifest,
       isActivity: (msg.type == 'activity'),
       target: msg.target,
-      expectingSystemMessage: true
+      expectingSystemMessage: true,
+      extra: msg.extra
     });
   },
 
   receiveMessage: function shell_receiveMessage(message) {
     var activities = { 'content-handler': { name: 'view', response: null },
                        'dial-handler':    { name: 'dial', response: null },
                        'mail-handler':    { name: 'new',  response: null },
                        'sms-handler':     { name: 'new',  response: null },
@@ -972,26 +973,29 @@ let RemoteDebugger = {
   },
 
   // Start the debugger server.
   start: function debugger_start() {
     if (!DebuggerServer.initialized) {
       // Ask for remote connections.
       DebuggerServer.init(this.prompt.bind(this));
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
-#ifndef MOZ_WIDGET_GONK
-      DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
-      DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger");
-      DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
-      DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
-#endif
-      if ("nsIProfiler" in Ci) {
-        DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
+      // Until we implement unix domain socket, we enable content actors
+      // only on development devices
+      if (Services.prefs.getBoolPref("devtools.debugger.enable-content-actors")) {
+        DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
+        DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger");
+        DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
+        DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
+        if ("nsIProfiler" in Ci) {
+          DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
+        }
+        DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
+        DebuggerServer.enableWebappsContentActor = true;
       }
-      DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
       DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js');
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     }
 
     let port = Services.prefs.getIntPref('devtools.debugger.remote-port') || 6000;
     try {
       DebuggerServer.openListener(port);
     } catch (e) {
--- a/b2g/components/TelURIParser.jsm
+++ b/b2g/components/TelURIParser.jsm
@@ -7,28 +7,28 @@
 this.EXPORTED_SYMBOLS = ["TelURIParser"];
 
 /**
  * Singleton providing functionality for parsing tel: and sms: URIs
  */
 this.TelURIParser = {
   parseURI: function(scheme, uri) {
     // https://www.ietf.org/rfc/rfc2806.txt
-    let subscriber = uri.slice((scheme + ':').length);
+    let subscriber = decodeURIComponent(uri.slice((scheme + ':').length));
 
     if (!subscriber.length) {
       return null;
     }
 
     let number = '';
     let pos = 0;
     let len = subscriber.length;
 
     // visual-separator
-    let visualSeparator = [ '-', '.', '(', ')' ];
+    let visualSeparator = [ ' ', '-', '.', '(', ')' ];
     let digits = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ];
     let dtmfDigits = [ '*', '#', 'A', 'B', 'C', 'D' ];
     let pauseCharacter = [ 'p', 'w' ];
 
     // global-phone-number
     if (subscriber[pos] == '+') {
       number += '+';
       for (++pos; pos < len; ++pos) {
--- a/b2g/components/test/unit/test_bug793310.js
+++ b/b2g/components/test/unit/test_bug793310.js
@@ -1,16 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function run_test() {
   Components.utils.import("resource:///modules/TelURIParser.jsm")
 
   // global-phone-number
-   do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234'), '+1234');
+  do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234'), '+1234');
+
+  // global-phone-number => white space separator
+  do_check_eq(TelURIParser.parseURI('tel', 'tel:+123 456 789'), '+123 456 789');
 
   // global-phone-number => ignored chars
   do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234_123'), '+1234');
 
   // global-phone-number => visualSeparator + digits
   do_check_eq(TelURIParser.parseURI('tel', 'tel:+-.()1234567890'), '+-.()1234567890');
 
   // local-phone-number
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "e04bb3527c33dd6771e63397a3b52a4b9a5fce4e", 
+    "revision": "878cc221e0fdadb4d42dc110945533104f6dd572", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/leo/releng-leo.tt
+++ b/b2g/config/leo/releng-leo.tt
@@ -1,12 +1,12 @@
 [
 {
-"size": 141304652,
-"digest": "0bbba7483a483803fa0277ddeeb3f8f9592b57cb83d57794c366fb8a3541b47f0ab901c071b7ceb6828bceec7170e5d5c05c5b1dcc926c70f3da46a7e224f078",
+"size": 117247732,
+"digest": "16e74278e4e9b0d710df77d68af1677c91823dccfc611ab00ee617298a63787f9f9892bd1a41eccb8d45fb18d61bfda0dbd1de88f1861c14b4b44da3b94a4eca",
 "algorithm": "sha512",
 "filename": "backup-leo.tar.xz"
 },
 {
 "size": 1570553,
 "digest": "ea03de74df73b05e939c314cd15c54aac7b5488a407b7cc4f5f263f3049a1f69642c567dd35c43d0bc3f0d599d0385a26ab2dd947a6b18f9044e4918b382eea7",
 "algorithm": "sha512",
 "filename": "Adreno200-AU_LINUX_ANDROID_ICS_CHOCO_CS.04.00.03.06.001.zip"
--- a/b2g/confvars.sh
+++ b/b2g/confvars.sh
@@ -50,9 +50,8 @@ MOZ_EXTENSION_MANAGER=1
 MOZ_TIME_MANAGER=1
 
 MOZ_B2G_CERTDATA=1
 MOZ_PAY=1
 MOZ_TOOLKIT_SEARCH=
 MOZ_PLACES=
 MOZ_B2G=1
 MOZ_FOLD_LIBS=1
-MOZ_WBMP=1
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -236,16 +236,19 @@ static int do_main(int argc, char* argv[
       // relaunches Metro Firefox with this command line arg.
       mainFlags = XRE_MAIN_FLAG_USE_METRO;
     } else {
       // This command-line flag is used to test the metro browser in a desktop
       // environment.
       for (int idx = 1; idx < argc; idx++) {
         if (IsArg(argv[idx], "metrodesktop")) {
           metroOnDesktop = true;
+          // Disable crash reporting when running in metrodesktop mode.
+          char crashSwitch[] = "MOZ_CRASHREPORTER_DISABLE=1";
+          putenv(crashSwitch);
           break;
         } 
       }
     }
   }
 #endif
 
   // Desktop browser launch
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -467,16 +467,20 @@ pref("general.warnOnAboutConfig",       
 pref("dom.disable_window_open_feature.location",  true);
 // prevent JS from setting status messages
 pref("dom.disable_window_status_change",          true);
 // allow JS to move and resize existing windows
 pref("dom.disable_window_move_resize",            false);
 // prevent JS from monkeying with window focus, etc
 pref("dom.disable_window_flip",                   true);
 
+// Disable touch events on Desktop Firefox by default until they are properly
+// supported (bug 736048)
+pref("dom.w3c_touch_events.enabled",        0);
+
 // popups.policy 1=allow,2=reject
 pref("privacy.popups.policy",               1);
 pref("privacy.popups.usecustom",            true);
 pref("privacy.popups.showBrowserMessage",   true);
 
 pref("privacy.item.cookies",                false);
 
 pref("privacy.clearOnShutdown.history",     true);
@@ -754,16 +758,22 @@ pref("browser.safebrowsing.reportURL", "
 pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
 pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
 pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
 pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
 pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
 
 pref("browser.safebrowsing.warning.infoURL", "https://www.mozilla.org/%LOCALE%/firefox/phishing-protection/");
 pref("browser.safebrowsing.malware.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
+// Since the application reputation query isn't hooked in anywhere yet, this
+// preference does not matter. To be extra safe, don't turn this preference on
+// for official builds without whitelisting (bug 842828).
+#ifndef MOZILLA_OFFICIAL
+pref("browser.safebrowsing.appRepURL", "https://sb-ssl.google.com/safebrowsing/clientreport/download");
+#endif
 
 #ifdef MOZILLA_OFFICIAL
 // Normally the "client ID" sent in updates is appinfo.name, but for
 // official Firefox releases from Mozilla we use a special identifier.
 pref("browser.safebrowsing.id", "navclient-auto-ffox");
 #endif
 
 // Name of the about: page contributed by safebrowsing to handle display of error
@@ -1268,8 +1278,12 @@ pref("media.webaudio.enabled", true);
 #endif
 
 // If this turns true, Moz*Gesture events are not called stopPropagation()
 // before content.
 pref("dom.debug.propagate_gesture_events_through_content", false);
 
 // The request URL of the GeoLocation backend.
 pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%GOOGLE_API_KEY%");
+
+// Necko IPC security checks only needed for app isolation for cookies/cache/etc:
+// currently irrelevant for desktop e10s
+pref("network.disable.ipc.security", true);
--- a/browser/base/content/abouthome/aboutHome.js
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -289,31 +289,59 @@ function ensureSnippetsMapThen(aCallback
 }
 
 function onSearchSubmit(aEvent)
 {
   let searchTerms = document.getElementById("searchText").value;
   let searchURL = document.documentElement.getAttribute("searchEngineURL");
 
   if (searchURL && searchTerms.length > 0) {
-    const SEARCH_TOKENS = {
-      "_searchTerms_": encodeURIComponent(searchTerms)
-    }
-    for (let key in SEARCH_TOKENS) {
-      searchURL = searchURL.replace(key, SEARCH_TOKENS[key]);
-    }
-
     // Send an event that a search was performed. This was originally
     // added so Firefox Health Report could record that a search from
     // about:home had occurred.
     let engineName = document.documentElement.getAttribute("searchEngineName");
     let event = new CustomEvent("AboutHomeSearchEvent", {detail: engineName});
     document.dispatchEvent(event);
 
-    window.location.href = searchURL;
+    const SEARCH_TOKEN = "_searchTerms_";
+    let searchPostData = document.documentElement.getAttribute("searchEnginePostData");
+    if (searchPostData) {
+      // Check if a post form already exists. If so, remove it.
+      const POST_FORM_NAME = "searchFormPost";
+      let form = document.forms[POST_FORM_NAME];
+      if (form) {
+        form.parentNode.removeChild(form);
+      }
+
+      // Create a new post form.
+      form = document.body.appendChild(document.createElement("form"));
+      form.setAttribute("name", POST_FORM_NAME);
+      // Set the URL to submit the form to.
+      form.setAttribute("action", searchURL.replace(SEARCH_TOKEN, searchTerms));
+      form.setAttribute("method", "post");
+
+      // Create new <input type=hidden> elements for search param.
+      searchPostData = searchPostData.split("&");
+      for (let postVar of searchPostData) {
+        let [name, value] = postVar.split("=");
+        if (value == SEARCH_TOKEN) {
+          value = searchTerms;
+        }
+        let input = document.createElement("input");
+        input.setAttribute("type", "hidden");
+        input.setAttribute("name", name);
+        input.setAttribute("value", value);
+        form.appendChild(input);
+      }
+      // Submit the form.
+      form.submit();
+   } else {
+      searchURL = searchURL.replace(SEARCH_TOKEN, encodeURIComponent(searchTerms));
+      window.location.href = searchURL;
+    }
   }
 
   aEvent.preventDefault();
 }
 
 
 function setupSearchEngine()
 {
--- a/browser/base/content/baseMenuOverlay.xul
+++ b/browser/base/content/baseMenuOverlay.xul
@@ -47,16 +47,21 @@
                   onclick="checkForMiddleClick(this, event);"
                   label="&productHelp.label;"
                   accesskey="&productHelp.accesskey;"
 #ifdef XP_MACOSX
                   key="key_openHelpMac"/>
 #else
                   />
 #endif
+        <menuitem id="menu_keyboardShortcuts"
+                  oncommand="openHelpLink('keyboard-shortcuts')"
+                  onclick="checkForMiddleClick(this, event);"
+                  label="&helpKeyboardShortcuts.label;"
+                  accesskey="&helpKeyboardShortcuts.accesskey;"/>
 #ifdef MOZ_SERVICES_HEALTHREPORT
         <menuitem id="healthReport"
                   label="&healthReport.label;"
                   accesskey="&healthReport.accesskey;"
                   oncommand="openHealthReport()"
                   onclick="checkForMiddleClick(this, event);"/>
 #endif
         <menuitem id="troubleShooting"
--- a/browser/base/content/browser-appmenu.inc
+++ b/browser/base/content/browser-appmenu.inc
@@ -366,16 +366,20 @@
             <menuitem id="appmenu_openHelp"
                       label="&helpMenu.label;"
                       oncommand="openHelpLink('firefox-help')"
                       onclick="checkForMiddleClick(this, event);"/>
             <menuitem id="appmenu_gettingStarted"
                       label="&appMenuGettingStarted.label;"
                       oncommand="gBrowser.loadOneTab('https://www.mozilla.org/firefox/central/', {inBackground: false});"
                       onclick="checkForMiddleClick(this, event);"/>
+            <menuitem id="appmenu_keyboardShortcuts"
+                      label="&helpKeyboardShortcuts.label;"
+                      oncommand="openHelpLink('keyboard-shortcuts')"
+                      onclick="checkForMiddleClick(this, event);"/>
 #ifdef MOZ_SERVICES_HEALTHREPORT
             <menuitem id="appmenu_healthReport"
                       label="&healthReport.label;"
                       oncommand="openHealthReport()"
                       onclick="checkForMiddleClick(this, event);"/>
 #endif
             <menuitem id="appmenu_troubleshootingInfo"
                       label="&helpTroubleshootingInfo.label;"
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -306,17 +306,17 @@
           <menuitem id="context-openframeintab"
                     label="&openFrameCmdInTab.label;"
                     accesskey="&openFrameCmdInTab.accesskey;"
                     oncommand="gContextMenu.openFrameInTab();"/>
           <menuitem id="context-openframe"
                     label="&openFrameCmd.label;"
                     accesskey="&openFrameCmd.accesskey;"
                     oncommand="gContextMenu.openFrame();"/>
-          <menuseparator/>
+          <menuseparator id="open-frame-sep"/>
           <menuitem id="context-reloadframe"
                     label="&reloadFrameCmd.label;"
                     accesskey="&reloadFrameCmd.accesskey;"
                     oncommand="gContextMenu.reloadFrame();"/>
           <menuseparator/>
           <menuitem id="context-bookmarkframe"
                     label="&bookmarkThisFrameCmd.label;"
                     accesskey="&bookmarkThisFrameCmd.accesskey;"
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -209,34 +209,16 @@
                               label="&historyButton.label;"/>
                     <menuitem id="menu_socialSidebar"
                               type="checkbox"
                               autocheck="false"
                               command="Social:ToggleSidebar"/>
                   </menupopup>
                 </menu>
                 <menuseparator/>
-                <menuitem id="menu_stop"
-                          class="show-only-for-keyboard"
-                          label="&stopCmd.label;"
-                          accesskey="&stopCmd.accesskey;"
-                          command="Browser:Stop"
-#ifdef XP_MACOSX
-                          key="key_stop_mac"/>
-#else
-                          key="key_stop"/>
-#endif
-                <menuitem id="menu_reload"
-                          class="show-only-for-keyboard"
-                          label="&reloadCmd.label;"
-                          accesskey="&reloadCmd.accesskey;"
-                          key="key_reload"
-                          command="Browser:ReloadOrDuplicate"
-                          onclick="checkForMiddleClick(this, event);"/>
-                <menuseparator class="show-only-for-keyboard"/>
                 <menu id="viewFullZoomMenu" label="&fullZoom.label;"
                       accesskey="&fullZoom.accesskey;"
                       onpopupshowing="FullZoom.updateMenu();">
                   <menupopup>
                     <menuitem id="menu_zoomEnlarge"
                               key="key_fullZoomEnlarge"
                               label="&fullZoomEnlargeCmd.label;"
                               accesskey="&fullZoomEnlargeCmd.accesskey;"
@@ -327,44 +309,16 @@
                          placespopup="true"
 #endif
                          oncommand="this.parentNode._placesView._onCommand(event);"
                          onclick="checkForMiddleClick(this, event);"
                          onpopupshowing="if (!this.parentNode._placesView)
                                            new HistoryMenu(event);"
                          tooltip="bhTooltip"
                          popupsinherittooltip="true">
-                <menuitem id="historyMenuBack"
-                          class="show-only-for-keyboard"
-                          label="&backCmd.label;"
-#ifdef XP_MACOSX
-                          key="goBackKb2"
-#else
-                          key="goBackKb"
-#endif
-                          command="Browser:BackOrBackDuplicate"
-                          onclick="checkForMiddleClick(this, event);"/>
-                <menuitem id="historyMenuForward"
-                          class="show-only-for-keyboard"
-                          label="&forwardCmd.label;"
-#ifdef XP_MACOSX
-                          key="goForwardKb2"
-#else
-                          key="goForwardKb"
-#endif
-                          command="Browser:ForwardOrForwardDuplicate"
-                          onclick="checkForMiddleClick(this, event);"/>
-                <menuitem id="historyMenuHome"
-                          class="show-only-for-keyboard"
-                          label="&historyHomeCmd.label;"
-                          oncommand="BrowserGoHome(event);"
-                          onclick="checkForMiddleClick(this, event);"
-                          key="goHome"/>
-                <menuseparator id="historyMenuHomeSeparator"
-                               class="show-only-for-keyboard"/>
                 <menuitem id="menu_showAllHistory"
                           label="&showAllHistoryCmd2.label;"
 #ifndef XP_MACOSX
                           key="showAllHistoryKb"
 #endif
                           command="Browser:ShowAllHistory"/>
                 <menuitem id="sanitizeItem"
                           label="&clearRecentHistory.label;"
@@ -483,24 +437,16 @@
             <menu id="tools-menu"
                   label="&toolsMenu.label;"
                   accesskey="&toolsMenu.accesskey;">
               <menupopup id="menu_ToolsPopup"
 #ifdef MOZ_SERVICES_SYNC
                          onpopupshowing="gSyncUI.updateUI();"
 #endif
                          >
-              <menuitem id="menu_search"
-                        class="show-only-for-keyboard"
-                        label="&search.label;"
-                        accesskey="&search.accesskey;"
-                        key="key_search"
-                        command="Tools:Search"/>
-              <menuseparator id="browserToolsSeparator"
-                             class="show-only-for-keyboard"/>
               <menuitem id="menu_openDownloads"
                         label="&downloads.label;"
                         accesskey="&downloads.accesskey;"
                         key="key_openDownloads"
                         command="Tools:Downloads"/>
               <menuitem id="menu_openAddons"
                         label="&addons.label;"
                         accesskey="&addons.accesskey;"
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -264,28 +264,28 @@ var gPluginHandler = {
         // plugin. Object tags can, and often do, deal with that themselves,
         // so don't stomp on the page developers toes.
         if (installable && !(plugin instanceof HTMLObjectElement)) {
           let installStatus = doc.getAnonymousElementByAttribute(plugin, "class", "installStatus");
           installStatus.setAttribute("installable", "true");
           let iconStatus = doc.getAnonymousElementByAttribute(plugin, "class", "icon");
           iconStatus.setAttribute("installable", "true");
 
-          let installLink = doc.getAnonymousElementByAttribute(plugin, "class", "installPluginLink");
+          let installLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "installPluginLink");
           this.addLinkClickCallback(installLink, "installSinglePlugin", plugin);
         }
         break;
 
       case "PluginBlocklisted":
       case "PluginOutdated":
         shouldShowNotification = true;
         break;
 
       case "PluginVulnerableUpdatable":
-        let updateLink = doc.getAnonymousElementByAttribute(plugin, "class", "checkForUpdatesLink");
+        let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
         this.addLinkClickCallback(updateLink, "openPluginUpdatePage");
         /* FALLTHRU */
 
       case "PluginVulnerableNoUpdate":
       case "PluginClickToPlay":
         this._handleClickToPlayEvent(plugin);
         let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
         let pluginName = this._getPluginInfo(plugin).pluginName;
@@ -301,17 +301,17 @@ var gPluginHandler = {
         shouldShowNotification = true;
         break;
 
       case "PluginPlayPreview":
         this._handlePlayPreviewEvent(plugin);
         break;
 
       case "PluginDisabled":
-        let manageLink = doc.getAnonymousElementByAttribute(plugin, "class", "managePluginsLink");
+        let manageLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "managePluginsLink");
         this.addLinkClickCallback(manageLink, "managePlugins");
         shouldShowNotification = true;
         break;
 
       case "PluginInstantiated":
       case "PluginRemoved":
         shouldShowNotification = true;
         break;
@@ -340,18 +340,18 @@ var gPluginHandler = {
     // if this isn't a known plugin, we can't activate it
     // (this also guards pluginHost.getPermissionStringForType against
     // unexpected input)
     if (!gPluginHandler.isKnownPlugin(objLoadingContent))
       return false;
 
     let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
     let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
-    let browser = gBrowser.getBrowserForDocument(objLoadingContent.ownerDocument.defaultView.top.document);
-    let pluginPermission = Services.perms.testPermission(browser.currentURI, permissionString);
+    let principal = objLoadingContent.ownerDocument.defaultView.top.document.nodePrincipal;
+    let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
 
     let isFallbackTypeValid =
       objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
       objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
 
     if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) {
       // checking if play preview is subject to CTP rules
       let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType);
@@ -506,17 +506,18 @@ var gPluginHandler = {
     let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document);
     let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
     let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
     // guard against giving pluginHost.getPermissionStringForType a type
     // not associated with any known plugin
     if (!gPluginHandler.isKnownPlugin(objLoadingContent))
       return;
     let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
-    let pluginPermission = Services.perms.testPermission(browser.currentURI, permissionString);
+    let principal = doc.defaultView.top.document.nodePrincipal;
+    let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
 
     let overlay = doc.getAnonymousElementByAttribute(aPlugin, "class", "mainBox");
 
     if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) {
       if (overlay)
         overlay.style.visibility = "hidden";
       return;
     }
@@ -625,23 +626,38 @@ var gPluginHandler = {
     }
     else if (event == "dismissed") {
       // Once the popup is dismissed, clicking the icon should show the full
       // list again
       this.options.primaryPlugin = null;
     }
   },
 
+  // Match the behaviour of nsPermissionManager
+  _getHostFromPrincipal: function PH_getHostFromPrincipal(principal) {
+    if (!principal.URI || principal.URI.schemeIs("moz-nullprincipal")) {
+      return "(null)";
+    }
+
+    try {
+      if (principal.URI.host)
+        return principal.URI.host;
+    } catch (e) {}
+
+    return principal.origin;
+  },
+
   _makeCenterActions: function PH_makeCenterActions(notification) {
-    let browser = notification.browser;
-    let contentWindow = browser.contentWindow;
+    let contentWindow = notification.browser.contentWindow;
     let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
 
-    let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(browser.currentURI);
+    let principal = contentWindow.document.nodePrincipal;
+    // This matches the behavior of nsPermssionManager, used for display purposes only
+    let principalHost = this._getHostFromPrincipal(principal);
 
     let centerActions = [];
     let pluginsFound = new Set();
     for (let plugin of cwu.plugins) {
       plugin.QueryInterface(Ci.nsIObjectLoadingContent);
       if (plugin.getContentTypeForMIMEType(plugin.actualType) != Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
         continue;
       }
@@ -661,17 +677,17 @@ var gPluginHandler = {
       // the tighter loop above.
       let permissionObj = Services.perms.
         getPermissionObject(principal, pluginInfo.permissionString, false);
       if (permissionObj) {
         pluginInfo.pluginPermissionHost = permissionObj.host;
         pluginInfo.pluginPermissionType = permissionObj.expireType;
       }
       else {
-        pluginInfo.pluginPermissionHost = browser.currentURI.host;
+        pluginInfo.pluginPermissionHost = principalHost;
         pluginInfo.pluginPermissionType = undefined;
       }
 
       let url;
       // TODO: allow the blocklist to specify a better link, bug 873093
       if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) {
         url = Services.urlFormatter.formatURLPref("plugins.update.url");
       }
@@ -724,28 +740,29 @@ var gPluginHandler = {
       case "continue":
         break;
       default:
         Cu.reportError(Error("Unexpected plugin state: " + aNewState));
         return;
     }
 
     let browser = aNotification.browser;
+    let contentWindow = browser.contentWindow;
     if (aNewState != "continue") {
-      Services.perms.add(browser.currentURI, aPluginInfo.permissionString,
-                         permission, expireType, expireTime);
+      let principal = contentWindow.document.nodePrincipal;
+      Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString,
+                                      permission, expireType, expireTime);
 
       if (aNewState == "block") {
         return;
       }
     }
 
     // Manually activate the plugins that would have been automatically
     // activated.
-    let contentWindow = browser.contentWindow;
     let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
     let plugins = cwu.plugins;
     let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
 
     for (let plugin of plugins) {
       plugin.QueryInterface(Ci.nsIObjectLoadingContent);
       // canActivatePlugin will return false if this isn't a known plugin type,
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -132,16 +132,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
   "resource:///modules/SitePermissions.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+  "resource:///modules/sessionstore/SessionStore.jsm");
+
 let gInitialPages = [
   "about:blank",
   "about:newtab",
   "about:home",
   "about:privatebrowsing",
   "about:welcomeback",
   "about:sessionrestore"
 ];
@@ -723,28 +726,16 @@ const gFormSubmitObserver = {
     }
 
     this.panel.openPopup(element, position, offset, 0);
   }
 };
 
 var gBrowserInit = {
   onLoad: function() {
-    // window.arguments[0]: URI to load (string), or an nsISupportsArray of
-    //                      nsISupportsStrings to load, or a xul:tab of
-    //                      a tabbrowser, which will be replaced by this
-    //                      window (for this case, all other arguments are
-    //                      ignored).
-    //                 [1]: character set (string)
-    //                 [2]: referrer (nsIURI)
-    //                 [3]: postData (nsIInputStream)
-    //                 [4]: allowThirdPartyFixup (bool)
-    if ("arguments" in window && window.arguments[0])
-      var uriToLoad = window.arguments[0];
-
     gMultiProcessBrowser = gPrefService.getBoolPref("browser.tabs.remote");
 
     var mustLoadSidebar = false;
 
     Cc["@mozilla.org/eventlistenerservice;1"]
       .getService(Ci.nsIEventListenerService)
       .addSystemEventListener(gBrowser, "click", contentAreaClick, true);
 
@@ -775,16 +766,17 @@ var gBrowserInit = {
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = window.XULBrowserWindow;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
       new nsBrowserAccess();
 
     // set default character set if provided
+    // window.arguments[1]: character set (string)
     if ("arguments" in window && window.arguments.length > 1 && window.arguments[1]) {
       if (window.arguments[1].startsWith("charset=")) {
         var arrayArgComponents = window.arguments[1].split("=");
         if (arrayArgComponents) {
           //we should "inherit" the charset menu setting in a new window
           getMarkupDocumentViewer().defaultCharacterSet = arrayArgComponents[1];
         }
       }
@@ -941,28 +933,28 @@ var gBrowserInit = {
     // Misc. inits.
     CombinedStopReload.init();
     TabsOnTop.init();
     gPrivateBrowsingUI.init();
     TabsInTitlebar.init();
     retrieveToolbarIconsizesFromTheme();
 
     // Wait until chrome is painted before executing code not critical to making the window visible
-    this._boundDelayedStartup = this._delayedStartup.bind(this, uriToLoad, mustLoadSidebar);
+    this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar);
     window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
 
     this._loadHandled = true;
   },
 
   _cancelDelayedStartup: function () {
     window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
     this._boundDelayedStartup = null;
   },
 
-  _delayedStartup: function(uriToLoad, mustLoadSidebar) {
+  _delayedStartup: function(mustLoadSidebar) {
     let tmp = {};
     Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
     let TelemetryTimestamps = tmp.TelemetryTimestamps;
     TelemetryTimestamps.add("delayedStartupStarted");
 
     this._cancelDelayedStartup();
 
     // We need to set the MozApplicationManifest event listeners up
@@ -971,16 +963,17 @@ var gBrowserInit = {
     // will be fired.
     gBrowser.addEventListener("MozApplicationManifest",
                               OfflineApps, false);
     // listen for offline apps on social
     let socialBrowser = document.getElementById("social-sidebar-browser");
     socialBrowser.addEventListener("MozApplicationManifest",
                               OfflineApps, false);
 
+    let uriToLoad = this._getUriToLoad();
     var isLoadingBlank = isBlankPageURL(uriToLoad);
 
     // This pageshow listener needs to be registered before we may call
     // swapBrowsersAndCloseOther() to receive pageshow events fired by that.
     gBrowser.addEventListener("pageshow", function(event) {
       // Filter out events that are not about the document load we are interested in
       if (content && event.target == content.document)
         setTimeout(pageShowEventHandlers, 0, event.persisted);
@@ -1007,16 +1000,19 @@ var gBrowserInit = {
 
         // Stop the about:blank load
         gBrowser.stop();
         // make sure it has a docshell
         gBrowser.docShell;
 
         gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, uriToLoad);
       }
+      // window.arguments[2]: referrer (nsIURI)
+      //                 [3]: postData (nsIInputStream)
+      //                 [4]: allowThirdPartyFixup (bool)
       else if (window.arguments.length >= 3) {
         loadURI(uriToLoad, window.arguments[2], window.arguments[3] || null,
                 window.arguments[4] || false);
         window.focus();
       }
       // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
       // Such callers expect that window.arguments[0] is handled as a single URI.
       else
@@ -1094,25 +1090,16 @@ var gBrowserInit = {
 
     // Bug 666804 - NetworkPrioritizer support for e10s
     if (!gMultiProcessBrowser) {
       let NP = {};
       Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP);
       NP.trackBrowserWindow(window);
     }
 
-    // initialize the session-restore service (in case it's not already running)
-    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
-    ss.init(window);
-
-    // Enable the Restore Last Session command if needed
-    if (ss.canRestoreLastSession &&
-        !PrivateBrowsingUtils.isWindowPrivate(window))
-      goSetCommandEnabled("Browser:RestoreLastSession", true);
-
     PlacesToolbarHelper.init();
 
     ctrlTab.readPref();
     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.
@@ -1164,17 +1151,16 @@ var gBrowserInit = {
     gSyncUI.init();
 #endif
 
 #ifdef MOZ_DATA_REPORTING
     gDataNotificationInfoBar.init();
 #endif
 
     gBrowserThumbnails.init();
-    TabView.init();
 
     setUrlAndSearchBarWidthForConditionalForwardButton();
     window.addEventListener("resize", function resizeHandler(event) {
       if (event.target == window)
         setUrlAndSearchBarWidthForConditionalForwardButton();
     });
 
     // Enable developer toolbar?
@@ -1279,21 +1265,56 @@ var gBrowserInit = {
 #ifdef MOZ_METRO
     gMetroPrefs.prefDomain.forEach(function(prefName) {
       gMetroPrefs.pushDesktopControlledPrefToMetro(prefName);
       Services.prefs.addObserver(prefName, gMetroPrefs, false);
     }, this);
 #endif
 #endif
 
+    SessionStore.promiseInitialized.then(() => {
+      // Enable the Restore Last Session command if needed
+      if (SessionStore.canRestoreLastSession &&
+          !PrivateBrowsingUtils.isWindowPrivate(window))
+        goSetCommandEnabled("Browser:RestoreLastSession", true);
+
+      TabView.init();
+
+      setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0);
+    });
+
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
-    setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0);
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
+  // Returns the URI(s) to load at startup.
+  _getUriToLoad: function () {
+    // window.arguments[0]: URI to load (string), or an nsISupportsArray of
+    //                      nsISupportsStrings to load, or a xul:tab of
+    //                      a tabbrowser, which will be replaced by this
+    //                      window (for this case, all other arguments are
+    //                      ignored).
+    if (!window.arguments || !window.arguments[0])
+      return null;
+
+    let uri = window.arguments[0];
+    let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"]
+                           .getService(Ci.nsISessionStartup);
+    let defaultArgs = Cc["@mozilla.org/browser/clh;1"]
+                        .getService(Ci.nsIBrowserHandler)
+                        .defaultArgs;
+
+    // If the given URI matches defaultArgs (the default homepage) we want
+    // to block its load if we're going to restore a session anyway.
+    if (uri == defaultArgs && sessionStartup.willOverrideHomepage)
+      return null;
+
+    return uri;
+  },
+
   onUnload: function() {
     // In certain scenarios it's possible for unload to be fired before onload,
     // (e.g. if the window is being closed after browser.js loads but before the
     // load completes). In that case, there's nothing to do here.
     if (!this._loadHandled)
       return;
 
     gDevToolsBrowser.forgetBrowserWindow(window);
@@ -2305,16 +2326,19 @@ function BrowserOnAboutPageLoad(doc) {
       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);
 
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -29,17 +29,19 @@ addMessageListener("Browser:HideSessionR
   let doc = content.document;
   let container;
   if (doc.documentURI.toLowerCase() == "about:home" &&
       (container = doc.getElementById("sessionRestoreContainer"))){
     container.hidden = true;
   }
 });
 
-addEventListener("DOMContentLoaded", function(event) {
-  LoginManagerContent.onContentLoaded(event);
-});
-addEventListener("DOMAutoComplete", function(event) {
-  LoginManagerContent.onUsernameInput(event);
-});
-addEventListener("blur", function(event) {
-  LoginManagerContent.onUsernameInput(event);
-});
+if (!Services.prefs.getBoolPref("browser.tabs.remote")) {
+  addEventListener("DOMContentLoaded", function(event) {
+    LoginManagerContent.onContentLoaded(event);
+  });
+  addEventListener("DOMAutoComplete", function(event) {
+    LoginManagerContent.onUsernameInput(event);
+  });
+  addEventListener("blur", function(event) {
+    LoginManagerContent.onUsernameInput(event);
+  });
+}
--- a/browser/base/content/newtab/dropTargetShim.js
+++ b/browser/base/content/newtab/dropTargetShim.js
@@ -18,59 +18,141 @@ let gDropTargetShim = {
   /**
    * The last drop target that was hovered.
    */
   _lastDropTarget: null,
 
   /**
    * Initializes the drop target shim.
    */
-  init: function DropTargetShim_init() {
-    let node = gGrid.node;
+  init: function () {
+    gGrid.node.addEventListener("dragstart", this, true);
+  },
+
+  /**
+   * Add all event listeners needed during a drag operation.
+   */
+  _addEventListeners: function () {
+    gGrid.node.addEventListener("dragend", this);
 
-    // Add drag event handlers.
-    node.addEventListener("dragstart", this, true);
-    node.addEventListener("dragend", this, true);
+    let docElement = document.documentElement;
+    docElement.addEventListener("dragover", this);
+    docElement.addEventListener("dragenter", this);
+    docElement.addEventListener("drop", this);
+  },
+
+  /**
+   * Remove all event listeners that were needed during a drag operation.
+   */
+  _removeEventListeners: function () {
+    gGrid.node.removeEventListener("dragend", this);
+
+    let docElement = document.documentElement;
+    docElement.removeEventListener("dragover", this);
+    docElement.removeEventListener("dragenter", this);
+    docElement.removeEventListener("drop", this);
   },
 
   /**
    * Handles all shim events.
    */
-  handleEvent: function DropTargetShim_handleEvent(aEvent) {
+  handleEvent: function (aEvent) {
     switch (aEvent.type) {
       case "dragstart":
-        this._start(aEvent);
+        this._dragstart(aEvent);
+        break;
+      case "dragenter":
+        aEvent.preventDefault();
         break;
       case "dragover":
         this._dragover(aEvent);
         break;
+      case "drop":
+        this._drop(aEvent);
+        break;
       case "dragend":
-        this._end(aEvent);
+        this._dragend(aEvent);
         break;
     }
   },
 
   /**
    * Handles the 'dragstart' event.
    * @param aEvent The 'dragstart' event.
    */
-  _start: function DropTargetShim_start(aEvent) {
+  _dragstart: function (aEvent) {
     if (aEvent.target.classList.contains("newtab-link")) {
       gGrid.lock();
+      this._addEventListeners();
+    }
+  },
 
-      // XXX bug 505521 - Listen for dragover on the document.
-      document.documentElement.addEventListener("dragover", this, false);
+  /**
+   * Handles the 'dragover' event.
+   * @param aEvent The 'dragover' event.
+   */
+  _dragover: function (aEvent) {
+    // XXX bug 505521 - Use the dragover event to retrieve the
+    //                  current mouse coordinates while dragging.
+    let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode;
+    gDrag.drag(sourceNode._newtabSite, aEvent);
+
+    // Find the current drop target, if there's one.
+    this._updateDropTarget(aEvent);
+
+    // If we have a valid drop target,
+    // let the drag-and-drop service know.
+    if (this._lastDropTarget) {
+      aEvent.preventDefault();
     }
   },
 
   /**
-   * Handles the 'drag' event and determines the current drop target.
-   * @param aEvent The 'drag' event.
+   * Handles the 'drop' event.
+   * @param aEvent The 'drop' event.
+   */
+  _drop: function (aEvent) {
+    // We're accepting all drops.
+    aEvent.preventDefault();
+
+    // Make sure to determine the current drop target
+    // in case the dragover event hasn't been fired.
+    this._updateDropTarget(aEvent);
+
+    // A site was successfully dropped.
+    this._dispatchEvent(aEvent, "drop", this._lastDropTarget);
+  },
+
+  /**
+   * Handles the 'dragend' event.
+   * @param aEvent The 'dragend' event.
    */
-  _drag: function DropTargetShim_drag(aEvent) {
+  _dragend: function (aEvent) {
+    if (this._lastDropTarget) {
+      if (aEvent.dataTransfer.mozUserCancelled) {
+        // The drag operation was cancelled.
+        this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+        this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+      }
+
+      // Clean up.
+      this._lastDropTarget = null;
+      this._cellPositions = null;
+    }
+
+    gGrid.unlock();
+    this._removeEventListeners();
+  },
+
+  /**
+   * Tries to find the current drop target and will fire
+   * appropriate dragenter, dragexit, and dragleave events.
+   * @param aEvent The current drag event.
+   */
+  _updateDropTarget: function (aEvent) {
     // Let's see if we find a drop target.
     let target = this._findDropTarget(aEvent);
 
     if (target != this._lastDropTarget) {
       if (this._lastDropTarget)
         // We left the last drop target.
         this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
 
@@ -82,63 +164,21 @@ let gDropTargetShim = {
         // We left the last drop target.
         this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
 
       this._lastDropTarget = target;
     }
   },
 
   /**
-   * Handles the 'dragover' event as long as bug 505521 isn't fixed to get
-   * current mouse cursor coordinates while dragging.
-   * @param aEvent The 'dragover' event.
-   */
-  _dragover: function DropTargetShim_dragover(aEvent) {
-    let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode;
-    gDrag.drag(sourceNode._newtabSite, aEvent);
-
-    this._drag(aEvent);
-  },
-
-  /**
-   * Handles the 'dragend' event.
-   * @param aEvent The 'dragend' event.
-   */
-  _end: function DropTargetShim_end(aEvent) {
-    // Make sure to determine the current drop target in case the dragenter
-    // event hasn't been fired.
-    this._drag(aEvent);
-
-    if (this._lastDropTarget) {
-      if (aEvent.dataTransfer.mozUserCancelled) {
-        // The drag operation was cancelled.
-        this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
-        this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
-      } else {
-        // A site was successfully dropped.
-        this._dispatchEvent(aEvent, "drop", this._lastDropTarget);
-      }
-
-      // Clean up.
-      this._lastDropTarget = null;
-      this._cellPositions = null;
-    }
-
-    gGrid.unlock();
-
-    // XXX bug 505521 - Remove the document's dragover listener.
-    document.documentElement.removeEventListener("dragover", this, false);
-  },
-
-  /**
    * Determines the current drop target by matching the dragged site's position
    * against all cells in the grid.
    * @return The currently hovered drop target or null.
    */
-  _findDropTarget: function DropTargetShim_findDropTarget() {
+  _findDropTarget: function () {
     // These are the minimum intersection values - we want to use the cell if
     // the site is >= 50% hovering its position.
     let minWidth = gDrag.cellWidth / 2;
     let minHeight = gDrag.cellHeight / 2;
 
     let cellPositions = this._getCellPositions();
     let rect = gTransformation.getNodePosition(gDrag.draggedSite.node);
 
@@ -169,20 +209,19 @@ let gDropTargetShim = {
   },
 
   /**
    * Dispatches a custom DragEvent on the given target node.
    * @param aEvent The source event.
    * @param aType The event type.
    * @param aTarget The target node that receives the event.
    */
-  _dispatchEvent:
-    function DropTargetShim_dispatchEvent(aEvent, aType, aTarget) {
-
+  _dispatchEvent: function (aEvent, aType, aTarget) {
     let node = aTarget.node;
     let event = document.createEvent("DragEvents");
 
-    event.initDragEvent(aType, true, true, window, 0, 0, 0, 0, 0, false, false,
+    // The event should not bubble to prevent recursion.
+    event.initDragEvent(aType, false, true, window, 0, 0, 0, 0, 0, false, false,
                         false, false, 0, node, aEvent.dataTransfer);
 
     node.dispatchEvent(event);
   }
 };
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -29,25 +29,33 @@ let gPage = {
       this._init();
 
     this._updateAttributes(enabled);
   },
 
   /**
    * Listens for notifications specific to this page.
    */
-  observe: function Page_observe() {
-    let enabled = gAllPages.enabled;
-    this._updateAttributes(enabled);
+  observe: function Page_observe(aSubject, aTopic, aData) {
+    if (aTopic == "nsPref:changed") {
+      let enabled = gAllPages.enabled;
+      this._updateAttributes(enabled);
 
-    // Initialize the whole page if we haven't done that, yet.
-    if (enabled) {
-      this._init();
-    } else {
-      gUndoDialog.hide();
+      // Initialize the whole page if we haven't done that, yet.
+      if (enabled) {
+        this._init();
+      } else {
+        gUndoDialog.hide();
+      }
+    } else if (aTopic == "page-thumbnail:create" && gGrid.ready) {
+      for (let site of gGrid.sites) {
+        if (site && site.url === aData) {
+          site.refreshThumbnail();
+        }
+      }
     }
   },
 
   /**
    * Updates the whole page and the grid when the storage has changed.
    */
   update: function Page_update() {
     // The grid might not be ready yet as we initialize it asynchronously.
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -126,19 +126,33 @@ Site.prototype = {
 
     let link = this._querySelector(".newtab-link");
     link.setAttribute("title", tooltip);
     link.setAttribute("href", url);
     this._querySelector(".newtab-title").textContent = title;
 
     if (this.isPinned())
       this._updateAttributes(true);
+    // request a staleness check for the thumbnail, which will cause page.js
+    // to be notified and call our refreshThumbnail() method.
+    PageThumbs.captureIfStale(this.url);
+    // but still display whatever thumbnail might be available now.
+    this.refreshThumbnail();
+  },
 
+  /**
+   * 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.
@@ -176,17 +190,14 @@ Site.prototype = {
         break;
       case "mouseover":
         this._node.removeEventListener("mouseover", this, false);
         this._speculativeConnect();
         break;
       case "dragstart":
         gDrag.start(this, aEvent);
         break;
-      case "drag":
-        gDrag.drag(this, aEvent);
-        break;
       case "dragend":
         gDrag.end(this, aEvent);
         break;
     }
   }
 };
--- a/browser/base/content/newtab/transformations.js
+++ b/browser/base/content/newtab/transformations.js
@@ -151,17 +151,17 @@ let gTransformation = {
     targetPosition.top += this._cellBorderWidths.top;
 
     // Nothing to do here if the positions already match.
     if (currentPosition.left == targetPosition.left &&
         currentPosition.top == targetPosition.top) {
       finish();
     } else {
       this.setSitePosition(aSite, targetPosition);
-      this._whenTransitionEnded(aSite.node, finish);
+      this._whenTransitionEnded(aSite.node, ["left", "top"], finish);
     }
   },
 
   /**
    * Rearranges a given array of sites and moves them to their new positions or
    * fades in/out new/removed sites.
    * @param aSites An array of sites to rearrange.
    * @param aOptions Set of options (see below).
@@ -197,25 +197,29 @@ let gTransformation = {
     let wait = Promise.promised(function () callback && callback());
     wait.apply(null, batch);
   },
 
   /**
    * Listens for the 'transitionend' event on a given node and calls the given
    * callback.
    * @param aNode The node that is transitioned.
+   * @param aProperties The properties we'll wait to be transitioned.
    * @param aCallback The callback to call when finished.
    */
   _whenTransitionEnded:
-    function Transformation_whenTransitionEnded(aNode, aCallback) {
+    function Transformation_whenTransitionEnded(aNode, aProperties, aCallback) {
 
-    aNode.addEventListener("transitionend", function onEnd() {
-      aNode.removeEventListener("transitionend", onEnd, false);
-      aCallback();
-    }, false);
+    let props = new Set(aProperties);
+    aNode.addEventListener("transitionend", function onEnd(e) {
+      if (props.has(e.propertyName)) {
+        aNode.removeEventListener("transitionend", onEnd);
+        aCallback();
+      }
+    });
   },
 
   /**
    * Gets a given node's opacity value.
    * @param aNode The node to get the opacity value from.
    * @return The node's opacity value.
    */
   _getNodeOpacity: function Transformation_getNodeOpacity(aNode) {
@@ -231,18 +235,19 @@ let gTransformation = {
    */
   _setNodeOpacity:
     function Transformation_setNodeOpacity(aNode, aOpacity, aCallback) {
 
     if (this._getNodeOpacity(aNode) == aOpacity) {
       if (aCallback)
         aCallback();
     } else {
-      if (aCallback)
-        this._whenTransitionEnded(aNode, aCallback);
+      if (aCallback) {
+        this._whenTransitionEnded(aNode, ["opacity"], aCallback);
+      }
 
       aNode.style.opacity = aOpacity;
     }
   },
 
   /**
    * Moves a site to the cell with the given index.
    * @param aSite The site to move.
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -272,16 +272,28 @@ nsContextMenu.prototype = {
                   !(this.isContentSelected || this.onTextInput || this.onLink ||
                     this.onImage || this.onVideo || this.onAudio || this.onSocial));
     this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
                                            !this.onSocial) || this.onPlainTextLink);
     this.showItem("context-searchselect", isTextSelected);
     this.showItem("context-keywordfield",
                   this.onTextInput && this.onKeywordField);
     this.showItem("frame", this.inFrame);
+
+    // srcdoc cannot be opened separately due to concerns about web
+    // content with about:srcdoc in location bar masquerading as trusted
+    // chrome/addon content.
+    // No need to also test for this.inFrame as this is checked in the parent
+    // submenu.
+    this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
+    this.showItem("context-openframeintab", !this.inSrcdocFrame);
+    this.showItem("context-openframe", !this.inSrcdocFrame);
+    this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
+    this.showItem("open-frame-sep", !this.inSrcdocFrame);
+
     this.showItem("frame-sep", this.inFrame && isTextSelected);
 
     // Hide menu entries for images, show otherwise
     if (this.inFrame) {
       if (mimeTypeIsTextBased(this.target.ownerDocument.contentType))
         this.isFrameImage.removeAttribute('hidden');
       else
         this.isFrameImage.setAttribute('hidden', 'true');
@@ -511,16 +523,17 @@ nsContextMenu.prototype = {
     this.onMailtoLink      = false;
     this.onSaveableLink    = false;
     this.link              = null;
     this.linkURL           = "";
     this.linkURI           = null;
     this.linkProtocol      = "";
     this.onMathML          = false;
     this.inFrame           = false;
+    this.inSrcdocFrame     = false;
     this.inSyntheticDoc    = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
     this.onEditableArea    = false;
     this.isDesignMode      = false;
     this.onCTPPlugin       = false;
 
     // Remember the node that was clicked.
@@ -674,21 +687,21 @@ nsContextMenu.prototype = {
     if ((this.target.nodeType == Node.TEXT_NODE &&
          this.target.parentNode.namespaceURI == NS_MathML)
          || (this.target.namespaceURI == NS_MathML))
       this.onMathML = true;
 
     // See if the user clicked in a frame.
     var docDefaultView = this.target.ownerDocument.defaultView;
     if (docDefaultView != docDefaultView.top) {
-      // srcdoc iframes are not considered frames for concerns about web
-      // content with about:srcdoc in location bar masqurading as trusted
-      // chrome/addon content.
-      if (!this.target.ownerDocument.isSrcdocDocument)
-        this.inFrame = true;
+      this.inFrame = true;
+
+      if (this.target.ownerDocument.isSrcdocDocument) {
+          this.inSrcdocFrame = true;
+      }
     }
 
     // if the document is editable, show context menu like in text inputs
     if (!this.onEditableArea) {
       var win = this.target.ownerDocument.defaultView;
       if (win) {
         var isEditable = false;
         try {
@@ -708,16 +721,17 @@ nsContextMenu.prototype = {
         if (isEditable) {
           this.onTextInput       = true;
           this.onKeywordField    = false;
           this.onImage           = false;
           this.onLoadedImage     = false;
           this.onCompletedImage  = false;
           this.onMathML          = false;
           this.inFrame           = false;
+          this.inSrcdocFrame     = false;
           this.hasBGImage        = false;
           this.isDesignMode      = true;
           this.onEditableArea = true;
           InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win));
           var canSpell = InlineSpellCheckerUI.canSpellCheck;
           InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
           this.showItem("spell-check-enabled", canSpell);
           this.showItem("spell-separator", canSpell);
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1462,17 +1462,18 @@
 
             b.droppedLinkHandler = handleDroppedLink;
 
             // If we just created a new tab that loads the default
             // newtab url, swap in a preloaded page if possible.
             // Do nothing if we're a private window.
             let docShellsSwapped = false;
             if (aURI == BROWSER_NEW_TAB_URL &&
-                !PrivateBrowsingUtils.isWindowPrivate(window)) {
+                !PrivateBrowsingUtils.isWindowPrivate(window) &&
+                !gMultiProcessBrowser) {
               docShellsSwapped = gBrowserNewTabPreloader.newTab(t);
             }
 
             // Dispatch a new tab notification.  We do this once we're
             // entirely done, so that things are in a consistent state
             // even if the event listener opens or closes tabs.
             var evt = document.createEvent("Events");
             evt.initEvent("TabOpen", true, false);
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -118,16 +118,17 @@ MOCHITEST_BROWSER_FILES = \
                  browser_bug537474.js \
                  browser_bug550565.js \
                  browser_bug553455.js \
                  browser_bug555224.js \
                  browser_bug555767.js \
                  browser_bug556061.js \
                  browser_bug559991.js \
                  browser_bug561623.js \
+                 browser_bug561636.js \
                  browser_bug562649.js \
                  browser_bug563588.js \
                  browser_bug565575.js \
                  browser_bug567306.js \
                  browser_bug575561.js \
                  browser_bug575830.js \
                  browser_bug577121.js \
                  browser_bug578534.js \
@@ -187,16 +188,17 @@ MOCHITEST_BROWSER_FILES = \
                  browser_bug887515.js \
                  browser_canonizeURL.js \
                  browser_clearplugindata_noage.html \
                  browser_clearplugindata.html \
                  browser_clearplugindata.js \
                  browser_contentAreaClick.js \
                  browser_contextSearchTabPosition.js \
                  browser_CTP_drag_drop.js \
+                 browser_CTP_data_urls.js \
                  browser_ctrlTab.js \
                  browser_customize_popupNotification.js \
                  browser_customize.js \
                  browser_disablechrome.js \
                  browser_discovery.js \
                  browser_duplicateIDs.js \
                  browser_findbarClose.js \
                  browser_fullscreen-window-open.js \
@@ -312,16 +314,17 @@ MOCHITEST_BROWSER_FILES = \
                  plugin_bug820497.html \
                  plugin_clickToPlayAllow.html \
                  plugin_clickToPlayDeny.html \
                  plugin_hidden_to_visible.html \
                  plugin_test.html \
                  plugin_test2.html \
                  plugin_test3.html \
                  plugin_two_types.html \
+                 plugin_data_url.html \
                  plugin_unknown.html \
                  pluginCrashCommentAndURL.html \
                  POSTSearchEngine.xml \
                  print_postdata.sjs \
                  redirect_bug623155.sjs \
                  test_bug435035.html \
                  test_bug462673.html \
                  test_bug628179.html \
@@ -345,22 +348,15 @@ MOCHITEST_BROWSER_FILES += \
 		browser_bug462289.js \
 		$(NULL)
 else
 MOCHITEST_BROWSER_FILES += \
 		browser_bug565667.js \
 		$(NULL)
 endif
 
-ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
-# Bug 766546.
-MOCHITEST_BROWSER_FILES += \
-  browser_bug561636.js \
-  $(NULL)
-endif
-
 ifdef MOZ_DATA_REPORTING
 MOCHITEST_BROWSER_FILES += \
   browser_datareporting_notification.js \
   $(NULL)
 endif
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_CTP_data_urls.js
@@ -0,0 +1,239 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir;
+const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+var gTestBrowser = null;
+var gNextTest = null;
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// This listens for the next opened tab and checks it is of the right url.
+// opencallback is called when the new tab is fully loaded
+// closecallback is called when the tab is closed
+function TabOpenListener(url, opencallback, closecallback) {
+  this.url = url;
+  this.opencallback = opencallback;
+  this.closecallback = closecallback;
+
+  gBrowser.tabContainer.addEventListener("TabOpen", this, false);
+}
+
+TabOpenListener.prototype = {
+  url: null,
+  opencallback: null,
+  closecallback: null,
+  tab: null,
+  browser: null,
+
+  handleEvent: function(event) {
+    if (event.type == "TabOpen") {
+      gBrowser.tabContainer.removeEventListener("TabOpen", this, false);
+      this.tab = event.originalTarget;
+      this.browser = this.tab.linkedBrowser;
+      gBrowser.addEventListener("pageshow", this, false);
+    } else if (event.type == "pageshow") {
+      if (event.target.location.href != this.url)
+        return;
+      gBrowser.removeEventListener("pageshow", this, false);
+      this.tab.addEventListener("TabClose", this, false);
+      var url = this.browser.contentDocument.location.href;
+      is(url, this.url, "Should have opened the correct tab");
+      this.opencallback(this.tab, this.browser.contentWindow);
+    } else if (event.type == "TabClose") {
+      if (event.originalTarget != this.tab)
+        return;
+      this.tab.removeEventListener("TabClose", this, false);
+      this.opencallback = null;
+      this.tab = null;
+      this.browser = null;
+      // Let the window close complete
+      executeSoon(this.closecallback);
+      this.closecallback = null;
+    }
+  }
+};
+
+function test() {
+  waitForExplicitFinish();
+  registerCleanupFunction(function() {
+    clearAllPluginPermissions();
+    Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+    getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+    getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+  });
+  Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+  var newTab = gBrowser.addTab();
+  gBrowser.selectedTab = newTab;
+  gTestBrowser = gBrowser.selectedBrowser;
+  gTestBrowser.addEventListener("load", pageLoad, true);
+
+  Services.prefs.setBoolPref("plugins.click_to_play", true);
+  getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+  getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+
+  prepareTest(test1a, gHttpTestRoot + "plugin_data_url.html");
+}
+
+function finishTest() {
+  clearAllPluginPermissions();
+  gTestBrowser.removeEventListener("load", pageLoad, true);
+  gBrowser.removeCurrentTab();
+  window.focus();
+  finish();
+}
+
+function pageLoad() {
+  // 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
+  executeSoon(gNextTest);
+}
+
+function prepareTest(nextTest, url) {
+  gNextTest = nextTest;
+  gTestBrowser.contentWindow.location = url;
+}
+
+// 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;
+  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");
+  let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated");
+
+  // Simulate clicking the "Allow Always" button.
+  popupNotification.reshow();
+  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");
+}
+
+// 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;
+  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
+  notification.reshow();
+  is(notification.options.centerActions.length, 2, "Test 2b, Should have two types of plugin in the notification");
+
+  var centerAction = null;
+  for (var action of notification.options.centerActions) {
+    if (action.pluginName == "Test") {
+      centerAction = action;
+      break;
+    }
+  }
+  ok(centerAction, "Test 2b, found center action for the Test plugin");
+
+  var centerItem = null;
+  for (var item of PopupNotifications.panel.firstChild.childNodes) {
+    is(item.value, "block", "Test 2b, all plugins should start out blocked");
+    if (item.action == centerAction) {
+      centerItem = item;
+      break;
+    }
+  }
+  ok(centerItem, "Test 2b, found center item for the Test plugin");
+
+  // "click" the button to activate the Test plugin
+  centerItem.value = "allownow";
+  PopupNotifications.panel.firstChild._primaryButton.click();
+
+  let plugin = gTestBrowser.contentDocument.getElementById("test1");
+  let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  let condition = function() objLoadingContent.activated;
+  waitForCondition(condition, test2c, "Test 2b, Waited too long for plugin to activate");
+}
+
+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");
+}
+
+// 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);
+  ok(!objLoadingContent.activated, "Test 3a, Plugin should not be activated");
+
+  // Simulate clicking the "Allow Always" button.
+  popupNotification.reshow();
+  PopupNotifications.panel.firstChild._primaryButton.click();
+
+  let condition = function() objLoadingContent.activated;
+  waitForCondition(condition, test3b, "Test 3a, Waited too long for plugin to activate");
+}
+
+function test3b() {
+  gNextTest = test3c;
+  gTestBrowser.contentDocument.getElementById("data-link-1").click();
+}
+
+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"/>');
+}
+
+// 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);
+  ok(!objLoadingContent.activated, "Test 4a, Plugin should not be activated");
+
+  // Simulate clicking the "Allow Always" button.
+  popupNotification.reshow();
+  PopupNotifications.panel.firstChild._primaryButton.click();
+
+  let condition = function() objLoadingContent.activated;
+  waitForCondition(condition, test4b, "Test 4a, Waited too long for plugin to activate");
+}
+
+function test4b() {
+  clearAllPluginPermissions();
+  finishTest();
+}
--- a/browser/base/content/test/browser_aboutHome.js
+++ b/browser/base/content/test/browser_aboutHome.js
@@ -237,28 +237,82 @@ let gTests = [
     }
     // Do a sanity check that all attributes are correctly set to begin with
     checkSearchUI(currEngine);
 
     let deferred = Promise.defer();
     promiseBrowserAttributes(gBrowser.selectedTab).then(function() {
       // Test if the update propagated
       checkSearchUI(unusedEngines[0]);
+      searchbar.currentEngine = currEngine;
       deferred.resolve();
     });
 
-    // The following cleanup function will set currentEngine back to the previous engine
+    // The following cleanup function will set currentEngine back to the previous
+    // engine if we fail to do so above.
     registerCleanupFunction(function() {
       searchbar.currentEngine = currEngine;
     });
     // Set the current search engine to an unused one
     searchbar.currentEngine = unusedEngines[0];
     searchbar.select();
     return deferred.promise;
   }
+},
+
+{
+  desc: "Check POST search engine support",
+  setup: function() {},
+  run: function()
+  {
+    let deferred = Promise.defer();
+    let currEngine = Services.search.defaultEngine;
+    let searchObserver = function search_observer(aSubject, aTopic, aData) {
+      let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+      info("Observer: " + aData + " for " + engine.name);
+
+      if (aData != "engine-added")
+        return;
+
+      if (engine.name != "POST Search")
+        return;
+
+      Services.search.defaultEngine = engine;
+
+      registerCleanupFunction(function() {
+        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");
+
+      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;
+  }
 }
 
 ];
 
 function test()
 {
   waitForExplicitFinish();
   requestLongerTimeout(2);
@@ -437,8 +491,20 @@ function getNumberOfSearchesByDate(aEngi
 
     if (day.has(field)) {
       return day.get(field) || 0;
     }
   }
 
   return 0; // No records found.
 }
+
+function waitForLoad(cb) {
+  let browser = gBrowser.selectedBrowser;
+  browser.addEventListener("load", function listener() {
+    if (browser.currentURI.spec == "about:blank")
+      return;
+    info("Page loaded: " + browser.currentURI.spec);
+    browser.removeEventListener("load", listener, true);
+
+    cb();
+  }, true);
+}
--- a/browser/base/content/test/browser_bug561636.js
+++ b/browser/base/content/test/browser_bug561636.js
@@ -369,22 +369,25 @@ function()
 
       nextTest();
     });
   };
 
   Services.obs.addObserver(gObserver, "invalidformsubmit", false);
 
   tab.linkedBrowser.addEventListener("load", function(e) {
-    let browser = e.currentTarget;
-    browser.removeEventListener("load", arguments.callee, true);
+    // Ignore load events from the iframe.
+    if (tab.linkedBrowser.contentDocument == e.target) {
+      let browser = e.currentTarget;
+      browser.removeEventListener("load", arguments.callee, true);
 
-    isnot(gBrowser.selectedTab.linkedBrowser, browser,
-          "This tab should have been loaded in background");
-    browser.contentDocument.getElementById('s').click();
+      isnot(gBrowser.selectedTab.linkedBrowser, browser,
+            "This tab should have been loaded in background");
+      browser.contentDocument.getElementById('s').click();
+    }
   }, true);
 
   tab.linkedBrowser.loadURI(uri);
 },
 
 /**
  * In this test, we check that the author defined error message is shown.
  */
--- a/browser/base/content/test/browser_pluginnotification.js
+++ b/browser/base/content/test/browser_pluginnotification.js
@@ -129,17 +129,17 @@ function test3() {
   ok(!gTestBrowser.missingPlugins, "Test 3, Should not be a missing plugin list");
 
   new TabOpenListener("about:addons", test4, prepareTest5);
 
   var pluginNode = gTestBrowser.contentDocument.getElementById("test");
   ok(pluginNode, "Test 3, Found plugin in page");
   var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED, "Test 3, plugin fallback type should be PLUGIN_DISABLED");
-  var manageLink = gTestBrowser.contentDocument.getAnonymousElementByAttribute(pluginNode, "class", "managePluginsLink");
+  var manageLink = gTestBrowser.contentDocument.getAnonymousElementByAttribute(pluginNode, "anonid", "managePluginsLink");
   ok(manageLink, "Test 3, found 'manage' link in plugin-problem binding");
 
   EventUtils.synthesizeMouseAtCenter(manageLink, {}, gTestBrowser.contentWindow);
 }
 
 function test4(tab, win) {
   is(win.wrappedJSObject.gViewController.currentViewId, "addons://list/plugin", "Test 4, Should have displayed the plugins pane");
   gBrowser.removeTab(tab);
@@ -339,17 +339,17 @@ function test18a() {
   var doc = gTestBrowser.contentDocument;
   var plugin = doc.getElementById("test");
   ok(plugin, "Test 18a, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "Test 18a, plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE");
   ok(!objLoadingContent.activated, "Test 18a, Plugin should not be activated");
   var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
   ok(overlay.style.visibility != "hidden", "Test 18a, Plugin overlay should exist, not be hidden");
-  var updateLink = doc.getAnonymousElementByAttribute(plugin, "class", "checkForUpdatesLink");
+  var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
   ok(updateLink.style.visibility != "hidden", "Test 18a, Plugin should have an update link");
 
   var tabOpenListener = new TabOpenListener(Services.urlFormatter.formatURLPref("plugins.update.url"), false, false);
   tabOpenListener.handleEvent = function(event) {
     if (event.type == "TabOpen") {
       gBrowser.tabContainer.removeEventListener("TabOpen", this, false);
       this.tab = event.originalTarget;
       ok(event.target.label == this.url, "Test 18a, Update link should open up the plugin check page");
@@ -382,17 +382,17 @@ function test18c() {
   var doc = gTestBrowser.contentDocument;
   var plugin = doc.getElementById("test");
   ok(plugin, "Test 18c, Found plugin in page");
   var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
   is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE, "Test 18c, plugin fallback type should be PLUGIN_VULNERABLE_NO_UPDATE");
   ok(!objLoadingContent.activated, "Test 18c, Plugin should not be activated");
   var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
   ok(overlay.style.visibility != "hidden", "Test 18c, Plugin overlay should exist, not be hidden");
-  var updateLink = doc.getAnonymousElementByAttribute(plugin, "class", "checkForUpdatesLink");
+  var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
   ok(updateLink.style.display != "block", "Test 18c, Plugin should not have an update link");
 
   // check that click "Always allow" works with blocklisted plugins
   clickToPlayNotification.reshow();
   PopupNotifications.panel.firstChild._primaryButton.click();
 
   var condition = function() objLoadingContent.activated;
   waitForCondition(condition, test18d, "Test 18d, Waited too long for plugin to activate");
--- a/browser/base/content/test/newtab/browser_newtab_bug735987.js
+++ b/browser/base/content/test/newtab/browser_newtab_bug735987.js
@@ -3,24 +3,30 @@
 
 function runTests() {
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8");
 
-  yield simulateDrop(1);
+  yield simulateExternalDrop(1);
   checkGrid("0,99p,1,2,3,4,5,6,7");
 
   yield blockCell(1);
   checkGrid("0,1,2,3,4,5,6,7,8");
 
-  yield simulateDrop(1);
+  yield simulateExternalDrop(1);
   checkGrid("0,99p,1,2,3,4,5,6,7");
 
+  // Simulate a restart and force the next about:newtab
+  // instance to read its data from the storage again.
   NewTabUtils.blockedLinks.resetCache();
+
+  // Update all open pages, e.g. preloaded ones.
+  NewTabUtils.allPages.update();
+
   yield addNewTabPageTab();
   checkGrid("0,99p,1,2,3,4,5,6,7");
 
   yield blockCell(1);
   checkGrid("0,1,2,3,4,5,6,7,8");
 }
--- a/browser/base/content/test/newtab/browser_newtab_drag_drop.js
+++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js
@@ -12,17 +12,17 @@ function runTests() {
 
   // test a simple drag-and-drop scenario
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8");
 
-  yield simulateDrop(1, 0);
+  yield simulateDrop(0, 1);
   checkGrid("1,0p,2,3,4,5,6,7,8");
 
   // drag a cell to its current cell and make sure it's not pinned afterwards
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8");
@@ -32,86 +32,86 @@ function runTests() {
 
   // ensure that pinned pages aren't moved if that's not necessary
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",1,2");
 
   yield addNewTabPageTab();
   checkGrid("0,1p,2p,3,4,5,6,7,8");
 
-  yield simulateDrop(3, 0);
+  yield simulateDrop(0, 3);
   checkGrid("3,1p,2p,0p,4,5,6,7,8");
 
   // pinned sites should always be moved around as blocks. if a pinned site is
   // moved around, neighboring pinned are affected as well
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("0,1");
 
   yield addNewTabPageTab();
   checkGrid("0p,1p,2,3,4,5,6,7,8");
 
-  yield simulateDrop(0, 2);
+  yield simulateDrop(2, 0);
   checkGrid("2p,0p,1p,3,4,5,6,7,8");
 
   // pinned sites should not be pushed out of the grid (unless there are only
   // pinned ones left on the grid)
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,7,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7p,8p");
 
-  yield simulateDrop(8, 2);
+  yield simulateDrop(2, 8);
   checkGrid("0,1,3,4,5,6,7p,8p,2p");
 
   // make sure that pinned sites are re-positioned correctly
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("0,1,2,,,5");
 
   yield addNewTabPageTab();
   checkGrid("0p,1p,2p,3,4,5p,6,7,8");
 
-  yield simulateDrop(4, 0);
+  yield simulateDrop(0, 4);
   checkGrid("3,1p,2p,4,0p,5p,6,7,8");
 
   // drag a new site onto the very first cell
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,7,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7p,8p");
 
-  yield simulateDrop(0);
+  yield simulateExternalDrop(0);
   checkGrid("99p,0,1,2,3,4,5,7p,8p");
 
   // drag a new site onto the grid and make sure that pinned cells don't get
   // pushed out
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,7,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7p,8p");
 
-  yield simulateDrop(7);
+  yield simulateExternalDrop(7);
   checkGrid("0,1,2,3,4,5,7p,99p,8p");
 
   // drag a new site beneath a pinned cell and make sure the pinned cell is
   // not moved
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks(",,,,,,,,8");
 
   yield addNewTabPageTab();
   checkGrid("0,1,2,3,4,5,6,7,8p");
 
-  yield simulateDrop(7);
+  yield simulateExternalDrop(7);
   checkGrid("0,1,2,3,4,5,6,99p,8p");
 
   // drag a new site onto a block of pinned sites and make sure they're shifted
   // around accordingly
   yield setLinks("0,1,2,3,4,5,6,7,8");
   setPinnedLinks("0,1,2,,,,,,");
 
   yield addNewTabPageTab();
   checkGrid("0p,1p,2p");
 
-  yield simulateDrop(1);
+  yield simulateExternalDrop(1);
   checkGrid("0p,99p,1p,2p,3,4,5,6,7");
 }
--- a/browser/base/content/test/newtab/browser_newtab_tabsync.js
+++ b/browser/base/content/test/newtab/browser_newtab_tabsync.js
@@ -38,22 +38,22 @@ function runTests() {
   // remove a cell
   yield blockCell(1);
   checkGrid("0,2,3,4,5,6,7,8,9");
   checkGrid("0,2,3,4,5,6,7,8,9", oldSites);
   ok(resetButton.hasAttribute("modified"), "page is modified");
   ok(oldResetButton.hasAttribute("modified"), "page is modified");
 
   // insert a new cell by dragging
-  yield simulateDrop(1);
+  yield simulateExternalDrop(1);
   checkGrid("0,99p,2,3,4,5,6,7,8");
   checkGrid("0,99p,2,3,4,5,6,7,8", oldSites);
 
   // drag a cell around
-  yield simulateDrop(1, 2);
+  yield simulateDrop(2, 1);
   checkGrid("0,2p,99p,3,4,5,6,7,8");
   checkGrid("0,2p,99p,3,4,5,6,7,8", oldSites);
 
   // reset the new tab page
   yield getContentWindow().gToolbar.reset(TestRunner.next);
   checkGrid("0,1,2,3,4,5,6,7,8");
   checkGrid("0,1,2,3,4,5,6,7,8", oldSites);
   ok(!resetButton.hasAttribute("modified"), "page is not modified");
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -1,26 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
 
 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
 
 let tmp = {};
+Cu.import("resource://gre/modules/Promise.jsm", tmp);
 Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
 Cc["@mozilla.org/moz/jssubscript-loader;1"]
   .getService(Ci.mozIJSSubScriptLoader)
   .loadSubScript("chrome://browser/content/sanitize.js", tmp);
-
-let {NewTabUtils, Sanitizer} = tmp;
+let {Promise, NewTabUtils, Sanitizer} = tmp;
 
 let uri = Services.io.newURI("about:newtab", null, null);
 let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
 
+let isMac = ("nsILocalFileMac" in Ci);
+let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc);
+let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
 let gWindow = window;
 
 registerCleanupFunction(function () {
   while (gWindow.gBrowser.tabs.length > 1)
     gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
 
   Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
 });
@@ -300,36 +303,176 @@ function pinCell(aIndex, aPinIndex) {
  * @param aIndex The cell index.
  */
 function unpinCell(aIndex) {
   whenPagesUpdated();
   getCell(aIndex).site.unpin();
 }
 
 /**
- * Simulates a drop and drop operation.
- * @param aDropIndex The cell index of the drop target.
- * @param aDragIndex The cell index containing the dragged site (optional).
+ * Simulates a drag and drop operation.
+ * @param aSourceIndex The cell index containing the dragged site.
+ * @param aDestIndex The cell index of the drop target.
+ */
+function simulateDrop(aSourceIndex, aDestIndex) {
+  let src = getCell(aSourceIndex).site.node;
+  let dest = getCell(aDestIndex).node;
+
+  // Drop 'src' onto 'dest' and continue testing when all newtab
+  // pages have been updated (i.e. the drop operation is completed).
+  startAndCompleteDragOperation(src, dest, whenPagesUpdated);
+}
+
+/**
+ * Simulates a drag and drop operation. Instead of rearranging a site that is
+ * is already contained in the newtab grid, this is used to simulate dragging
+ * an external link onto the grid e.g. the text from the URL bar.
+ * @param aDestIndex The cell index of the drop target.
  */
-function simulateDrop(aDropIndex, aDragIndex) {
-  let draggedSite;
-  let {gDrag: drag, gDrop: drop} = getContentWindow();
-  let event = createDragEvent("drop", "http://example.com/#99\nblank");
+function simulateExternalDrop(aDestIndex) {
+  let dest = getCell(aDestIndex).node;
+
+  // Create an iframe that contains the external link we'll drag.
+  createExternalDropIframe().then(iframe => {
+    let link = iframe.contentDocument.getElementById("link");
+
+    // Drop 'link' onto 'dest'.
+    startAndCompleteDragOperation(link, dest, () => {
+      // Wait until the drop operation is complete
+      // and all newtab pages have been updated.
+      whenPagesUpdated(() => {
+        // Clean up and remove the iframe.
+        iframe.remove();
+        // Continue testing.
+        TestRunner.next();
+      });
+    });
+  });
+}
+
+/**
+ * Starts and complete a drag-and-drop operation.
+ * @param aSource The node that is being dragged.
+ * @param aDest The node we're dragging aSource onto.
+ * @param aCallback The function that is called when we're done.
+ */
+function startAndCompleteDragOperation(aSource, aDest, aCallback) {
+  // Start by pressing the left mouse button.
+  synthesizeNativeMouseLDown(aSource);
+
+  // Move the mouse in 5px steps until the drag operation starts.
+  let offset = 0;
+  let interval = setInterval(() => {
+    synthesizeNativeMouseDrag(aSource, offset += 5);
+  }, 10);
+
+  // When the drag operation has started we'll move
+  // the dragged element to its target position.
+  aSource.addEventListener("dragstart", function onDragStart() {
+    aSource.removeEventListener("dragstart", onDragStart);
+    clearInterval(interval);
+
+    // Place the cursor above the drag target.
+    synthesizeNativeMouseMove(aDest);
+  });
+
+  // As soon as the dragged element hovers the target, we'll drop it.
+  aDest.addEventListener("dragenter", function onDragEnter() {
+    aDest.removeEventListener("dragenter", onDragEnter);
+
+    // Finish the drop operation.
+    synthesizeNativeMouseLUp(aDest);
+    aCallback();
+  });
+}
 
-  if (typeof aDragIndex != "undefined")
-    draggedSite = getCell(aDragIndex).site;
+/**
+ * Helper function that creates a temporary iframe in the about:newtab
+ * document. This will contain a link we can drag to the test the dropping
+ * of links from external documents.
+ */
+function createExternalDropIframe() {
+  const url = "data:text/html;charset=utf-8," +
+              "<a id='link' href='http://example.com/%2399'>link</a>";
+
+  let deferred = Promise.defer();
+  let doc = getContentDocument();
+  let iframe = doc.createElement("iframe");
+  iframe.setAttribute("src", url);
+  iframe.style.width = "50px";
+  iframe.style.height = "50px";
+
+  let margin = doc.getElementById("newtab-margin-top");
+  margin.appendChild(iframe);
 
-  if (draggedSite)
-    drag.start(draggedSite, event);
+  iframe.addEventListener("load", function onLoad() {
+    iframe.removeEventListener("load", onLoad);
+    executeSoon(() => deferred.resolve(iframe));
+  });
+
+  return deferred.promise;
+}
+
+/**
+ * Fires a synthetic 'mousedown' event on the current about:newtab page.
+ * @param aElement The element used to determine the cursor position.
+ */
+function synthesizeNativeMouseLDown(aElement) {
+  if (isLinux) {
+    let win = aElement.ownerDocument.defaultView;
+    EventUtils.synthesizeMouseAtCenter(aElement, {type: "mousedown"}, win);
+  } else {
+    let msg = isWindows ? 2 : 1;
+    synthesizeNativeMouseEvent(aElement, msg);
+  }
+}
 
-  whenPagesUpdated();
-  drop.drop(getCell(aDropIndex), event);
+/**
+ * Fires a synthetic 'mouseup' event on the current about:newtab page.
+ * @param aElement The element used to determine the cursor position.
+ */
+function synthesizeNativeMouseLUp(aElement) {
+  let msg = isWindows ? 4 : (isMac ? 2 : 7);
+  synthesizeNativeMouseEvent(aElement, msg);
+}
+
+/**
+ * Fires a synthetic mouse drag event on the current about:newtab page.
+ * @param aElement The element used to determine the cursor position.
+ * @param aOffsetX The left offset that is added to the position.
+ */
+function synthesizeNativeMouseDrag(aElement, aOffsetX) {
+  let msg = isMac ? 6 : 1;
+  synthesizeNativeMouseEvent(aElement, msg, aOffsetX);
+}
 
-  if (draggedSite)
-    drag.end(draggedSite);
+/**
+ * Fires a synthetic 'mousemove' event on the current about:newtab page.
+ * @param aElement The element used to determine the cursor position.
+ */
+function synthesizeNativeMouseMove(aElement) {
+  let msg = isMac ? 5 : 1;
+  synthesizeNativeMouseEvent(aElement, msg);
+}
+
+/**
+ * Fires a synthetic mouse event on the current about:newtab page.
+ * @param aElement The element used to determine the cursor position.
+ * @param aOffsetX The left offset that is added to the position (optional).
+ * @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);
 }
 
 /**
  * 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).
  */
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/plugin_data_url.html
@@ -0,0 +1,11 @@
+<html>
+<body>
+  <a id="data-link-1" href='data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'>
+    data: with one plugin
+  </a><br />
+  <a id="data-link-2" href='data:text/html,<embed id="test1" style="width: 200px; height: 200px" type="application/x-test"/><embed id="test2" style="width: 200px; height: 200px" type="application/x-second-test"/>'>
+    data: with two plugins
+  </a><br />
+  <object id="test" style="width: 200px; height: 200px" type="application/x-test"></object>
+</body>
+</html>
--- a/browser/base/content/test/social/browser_social_window.js
+++ b/browser/base/content/test/social/browser_social_window.js
@@ -21,18 +21,17 @@ let createdWindows = [];
 function openWindowAndWaitForInit(callback) {
   // this notification tells us SocialUI.init() has been run...
   let topic = "browser-delayed-startup-finished";
   let w = OpenBrowserWindow();
   createdWindows.push(w);
   Services.obs.addObserver(function providerSet(subject, topic, data) {
     Services.obs.removeObserver(providerSet, topic);
     info(topic + " observer was notified - continuing test");
-    // executeSoon to let the browser UI observers run first
-    executeSoon(function() {callback(w)});
+    executeSoon(() => callback(w));
   }, topic, false);
 }
 
 function postTestCleanup(cb) {
   for (let w of createdWindows)
     w.close();
   createdWindows = [];
   Services.prefs.clearUserPref("social.enabled");
--- a/browser/base/content/test/subtst_contextmenu.html
+++ b/browser/base/content/test/subtst_contextmenu.html
@@ -62,10 +62,11 @@ Browser context menu subtest.
 </div>
 <div id="test-select-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
 <div id="test-select-text-link">http://mozilla.com</div>
 <a id="test-image-link" href="#"><img src="ctxmenu-image.png"></a>
 <input id="test-select-input-text" type="text" value="input">
 <input id="test-select-input-text-type-password" type="password" value="password">
 <embed id="test-plugin" style="width: 200px; height: 200px;" type="application/x-test"></embed>
 <img id="test-longdesc" src="ctxmenu-image.png" longdesc="http://www.mozilla.org"></embed>
+<iframe id="test-srcdoc" width="98"  height="98" srcdoc="Hello World" style="border: 1px solid black"></iframe>
 </body>
 </html>
--- a/browser/base/content/test/test_contextmenu.html
+++ b/browser/base/content/test/test_contextmenu.html
@@ -950,16 +950,43 @@ function runTest(testNum) {
                           "---",                          null,
                           "context-saveimage",            true,
                           "context-sendimage",            true,
                           "context-setDesktopBackground", true,
                           "context-viewimageinfo",        true,
                           "context-viewimagedesc",        true
                          ].concat(inspectItems));
         closeContextMenu();
+        openContextMenuFor(srcdoc);
+        return;
+
+    case 31:
+        // Context menu for an iframe with srcdoc attribute set
+        checkContextMenu(["context-back",         false,
+                          "context-forward",      false,
+                          "context-reload",       true,
+                          "---",                  null,
+                          "context-bookmarkpage", true,
+                          "context-savepage",     true,
+                          "---",                  null,
+                          "context-viewbgimage",  false,
+                          "context-selectall",    true,
+                          "frame",                null,
+                              ["context-reloadframe",       true,
+                               "---",                       null,
+                               "context-saveframe",         true,
+                               "---",                       null,
+                               "context-printframe",        true,
+                               "---",                       null,
+                               "context-viewframesource",   true,
+                               "context-viewframeinfo",     true], null,
+                          "---",                  null,
+                          "context-viewsource",   true,
+                          "context-viewinfo",     true
+        ].concat(inspectItems));
 
         // finish test
         subwindow.close();
         SimpleTest.finish();
         return;
 
     /*
      * Other things that would be nice to test:
@@ -979,17 +1006,17 @@ function runTest(testNum) {
 
 
 var testNum = 1;
 var subwindow, chromeWin, contextMenu, lastElement;
 var text, link, mailto, input, img, canvas, video_ok, video_bad, video_bad2,
     iframe, video_in_iframe, image_in_iframe, textarea, contenteditable,
     inputspell, pagemenu, dom_full_screen, plainTextItems, audio_in_video,
     selecttext, selecttextlink, imagelink, select_inputtext, select_inputtext_password,
-    plugin, longdesc;
+    plugin, longdesc, iframe;
 
 function startTest() {
     chromeWin = SpecialPowers.wrap(subwindow)
                     .QueryInterface(Ci.nsIInterfaceRequestor)
                     .getInterface(Ci.nsIWebNavigation)
                     .QueryInterface(Ci.nsIDocShellTreeItem)
                     .rootTreeItem
                     .QueryInterface(Ci.nsIInterfaceRequestor)
@@ -1029,16 +1056,17 @@ function startTest() {
     pagemenu = subwindow.document.getElementById("test-pagemenu");
     dom_full_screen = subwindow.document.getElementById("test-dom-full-screen");
     selecttext = subwindow.document.getElementById("test-select-text");
     selecttextlink = subwindow.document.getElementById("test-select-text-link");
     select_inputtext = subwindow.document.getElementById("test-select-input-text");
     select_inputtext_password = subwindow.document.getElementById("test-select-input-text-type-password");
     plugin = subwindow.document.getElementById("test-plugin");
     longdesc = subwindow.document.getElementById("test-longdesc");
+    srcdoc = subwindow.document.getElementById("test-srcdoc");
 
     contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }, false);
     runTest(1);
 }
 
 // We open this in a separate window, because the Mochitests run inside a frame.
 // The frame causes an extra menu item, and prevents running the test
 // standalone (ie, clicking the test name in the Mochitest window) to see
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1595,17 +1595,18 @@
           var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box");
 
           if (this._states.SINGLE == state) {
             grid.hidden = true;
             this._setupSingleState();
             return;
           }
 
-          this._setupDescription("pluginActivateMultiple.message");
+          let host = gPluginHandler._getHostFromPrincipal(this.notification.browser.contentWindow.document.nodePrincipal);
+          this._setupDescription("pluginActivateMultiple.message", null, host);
 
           var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox");
 
           var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties");
           this._primaryButton.label = dialogStrings.GetStringFromName("button-accept");
           this._primaryButton.setAttribute("default", "true");
 
           this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel");
@@ -1758,19 +1759,16 @@
         <parameter name="host" />
         <body><![CDATA[
           var bsn = this._brandShortName;
           var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
           while (span.lastChild) {
             span.removeChild(span.lastChild);
           }
 
-          if (!host) {
-            host = this.notification.browser.currentURI.host;
-          }
           var args = ["__host__", this._brandShortName];
           if (pluginName) {
             args.unshift(pluginName);
           }
           var bases = gNavigatorBundle.getFormattedString(baseString, args).
             split("__host__", 2);
 
           span.appendChild(document.createTextNode(bases[0]));
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -13,17 +13,18 @@ XPCOMUtils.defineLazyGetter(this, "BROWS
   const PREF = "browser.newtab.url";
 
   function getNewTabPageURL() {
     if (!Services.prefs.prefHasUserValue(PREF)) {
       if (PrivateBrowsingUtils.isWindowPrivate(window) &&
           !PrivateBrowsingUtils.permanentPrivateBrowsing)
         return "about:privatebrowsing";
     }
-    return Services.prefs.getCharPref(PREF) || "about:blank";
+    let url = Services.prefs.getComplexValue(PREF, Ci.nsISupportsString).data;
+    return url || "about:blank";
   }
 
   function update() {
     BROWSER_NEW_TAB_URL = getNewTabPageURL();
   }
 
   Services.prefs.addObserver(PREF, update, false);
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -63,16 +63,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
                                   "resource://gre/modules/PlacesBackups.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+                                  "resource:///modules/sessionstore/SessionStore.jsm");
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // We try to backup bookmarks at idle times, to avoid doing that at shutdown.
 // Number of idle seconds before trying to backup bookmarks.  15 minutes.
 const BOOKMARKS_BACKUP_IDLE_TIME = 15 * 60;
 // Minimum interval in milliseconds between backups.
 const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000;
@@ -168,17 +171,17 @@ BrowserGlue.prototype = {
     switch (topic) {
       case "prefservice:after-app-defaults":
         this._onAppDefaults();
         break;
       case "final-ui-startup":
         this._finalUIStartup();
         break;
       case "browser-delayed-startup-finished":
-        this._onFirstWindowLoaded();
+        this._onFirstWindowLoaded(subject);
         Services.obs.removeObserver(this, "browser-delayed-startup-finished");
         break;
       case "sessionstore-windows-restored":
         this._onWindowsRestored();
         break;
       case "browser:purge-session-history":
         // reset the console service's error buffer
         Services.console.logStringMessage(null); // clear the console (in case it's open)
@@ -574,36 +577,36 @@ BrowserGlue.prototype = {
 
     let nb = win.document.getElementById("global-notificationbox");
     nb.appendNotification(message, "reset-unused-profile",
                           "chrome://global/skin/icons/question-16.png",
                           nb.PRIORITY_INFO_LOW, buttons);
   },
 
   // the first browser window has finished initializing
-  _onFirstWindowLoaded: function BG__onFirstWindowLoaded() {
+  _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) {
 #ifdef XP_WIN
     // For windows seven, initialize the jump list module.
     const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
     if (WINTASKBAR_CONTRACTID in Cc &&
         Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) {
       let temp = {};
       Cu.import("resource:///modules/WindowsJumpLists.jsm", temp);
       temp.WinTaskbarJumpList.startup();
     }
 #endif
 
+    SessionStore.init(aWindow);
     this._trackSlowStartup();
 
     // Offer to reset a user's profile if it hasn't been used for 60 days.
     const OFFER_PROFILE_RESET_INTERVAL_MS = 60 * 24 * 60 * 60 * 1000;
-    let processStartupTime = Services.startup.getStartupInfo().process;
     let lastUse = Services.appinfo.replacedLockTime;
-    if (processStartupTime && lastUse &&
-        processStartupTime.getTime() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) {
+    if (lastUse &&
+        Date.now() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) {
       this._resetUnusedProfileNotification();
     }
   },
 
   /**
    * Profile shutdown handler (contains profile cleanup routines).
    * All components depending on Places should be shut down in
    * _onPlacesShutdown() and not here.
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -114,23 +114,16 @@ var PlacesOrganizer = {
     var findKey = document.getElementById("key_find");
     findKey.setAttribute("command", "OrganizerCommand_find:all");
 
     // 2. Disable some keybindings from browser.xul
     var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"];
     for (var i=0; i < elements.length; i++) {
       document.getElementById(elements[i]).setAttribute("disabled", "true");
     }
-    
-    // 3. Disable the keyboard shortcut for the History menu back/forward
-    // in order to support those in the Library
-    var historyMenuBack = document.getElementById("historyMenuBack");
-    historyMenuBack.removeAttribute("key");
-    var historyMenuForward = document.getElementById("historyMenuForward");
-    historyMenuForward.removeAttribute("key");
 #endif
 
     // remove the "Properties" context-menu item, we've our own details pane
     document.getElementById("placesContext")
             .removeChild(document.getElementById("placesContext_show:info"));
 
     ContentArea.focus();
   },
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -1,40 +1,77 @@
 /* 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/. */
 
 function debug(msg) {
   Services.console.logStringMessage("SessionStoreContent: " + msg);
 }
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
 /**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 let EventListener = {
 
   DOM_EVENTS: [
-    "pageshow", "change", "input"
+    "pageshow", "change", "input", "MozStorageChanged"
   ],
 
   init: function () {
     this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
   },
 
   handleEvent: function (event) {
     switch (event.type) {
       case "pageshow":
         if (event.persisted)
           sendAsyncMessage("SessionStore:pageshow");
         break;
       case "input":
       case "change":
         sendAsyncMessage("SessionStore:input");
         break;
+      case "MozStorageChanged": {
+        let isSessionStorage = true;
+        // We are only interested in sessionStorage events
+        try {
+          if (event.storageArea != content.sessionStorage) {
+            isSessionStorage = false;
+          }
+        } catch (ex) {
+          // This page does not even have sessionStorage
+          // (this is typically the case of about: pages)
+          isSessionStorage = false;
+        }
+        if (isSessionStorage) {
+          sendAsyncMessage("SessionStore:MozStorageChanged");
+        }
+        break;
+      }
       default:
         debug("received unknown event '" + event.type + "'");
         break;
     }
   }
 };
+EventListener.init();
 
-EventListener.init();
+let ProgressListener = {
+  init: function() {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+  },
+  onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+    // We are changing page, so time to invalidate the state of the tab
+    sendAsyncMessage("SessionStore:loadStart");
+  },
+  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {},
+  onProgressChange: function() {},
+  onStatusChange: function() {},
+  onSecurityChange: function() {},
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference])
+};
+ProgressListener.init();
--- a/browser/components/sessionstore/nsISessionStartup.idl
+++ b/browser/components/sessionstore/nsISessionStartup.idl
@@ -5,34 +5,48 @@
 #include "nsISupports.idl"
 
 /**
  * nsISessionStore keeps track of the current browsing state - i.e.
  * tab history, cookies, scroll state, form data, POSTDATA and window features
  * - and allows to restore everything into one window.
  */
 
-[scriptable, uuid(35235b39-7098-4b3b-8e28-cd004a88b06f)]
+[scriptable, uuid(51f4b9f0-f3d2-11e2-bb62-2c24dd830245)]
 interface nsISessionStartup: nsISupports
 {
   /**
    * Return a promise that is resolved once initialization
    * is complete.
    */
   readonly attribute jsval onceInitialized;
 
   // Get session state
   readonly attribute jsval state;
 
   /**
-   * Determine if session should be restored
+   * Determines whether there is a pending session restore and makes sure that
+   * we're initialized before returning. If we're not yet this will read the
+   * session file synchronously.
    */
   boolean doRestore();
 
   /**
+   * Returns whether we will restore a session that ends up replacing the
+   * homepage. The browser uses this to not start loading the homepage if
+   * we're going to stop its load anyway shortly after.
+   *
+   * This is meant to be an optimization for the average case that loading the
+   * session file finishes before we may want to start loading the default
+   * homepage. Should this be called before the session file has been read it
+   * will just return false.
+   */
+  readonly attribute bool willOverrideHomepage;
+
+  /**
    * What type of session we're restoring.
    * NO_SESSION       There is no data available from the previous session
    * RECOVER_SESSION  The last session crashed. It will either be restored or
    *                  about:sessionrestore will be shown.
    * RESUME_SESSION   The previous session should be restored at startup
    * DEFER_SESSION    The previous session is fine, but it shouldn't be restored
    *                  without explicit action (with the exception of pinned tabs)
    */
--- a/browser/components/sessionstore/nsISessionStore.idl
+++ b/browser/components/sessionstore/nsISessionStore.idl
@@ -20,25 +20,20 @@ interface nsIDOMNode;
  * global |window| object to the API, though (or |top| from a sidebar).
  * From elsewhere you can get browser windows through the nsIWindowMediator
  * by looking for "navigator:browser" windows.
  *
  * * "Tabbrowser tabs" are all the child nodes of a browser window's
  * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|.
  */
 
-[scriptable, uuid(0aa5492c-15ad-4376-8eac-28895796826e)]
+[scriptable, uuid(700756cc-f5c7-11e2-b842-59d9dc830245)]
 interface nsISessionStore : nsISupports
 {
   /**
-   * Initialize the service
-   */
-  void init(in nsIDOMWindow aWindow);
-
-  /**
    * Is it possible to restore the previous session. Will always be false when
    * in Private Browsing mode.
    */
   attribute boolean canRestoreLastSession;
 
   /**
    * Restore the previous session if possible. This will not overwrite the
    * current session. Instead the previous session will be merged into the
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -53,17 +53,25 @@ const MESSAGES = [
   // The content script tells us that its form data (or that of one of its
   // subframes) might have changed. This can be the contents or values of
   // standard form fields or of ContentEditables.
   "SessionStore:input",
 
   // The content script has received a pageshow event. This happens when a
   // page is loaded from bfcache without any network activity, i.e. when
   // clicking the back or forward button.
-  "SessionStore:pageshow"
+  "SessionStore:pageshow",
+
+  // The content script has received a MozStorageChanged event dealing
+  // with a change in the contents of the sessionStorage.
+  "SessionStore:MozStorageChanged",
+
+  // The content script tells us that a new page just started loading in a
+  // browser.
+  "SessionStore:loadStart"
 ];
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
@@ -74,17 +82,17 @@ const TAB_EVENTS = [
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 // debug.js adds NS_ASSERT. cf. bug 669196
 Cu.import("resource://gre/modules/debug.js", this);
 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 
 XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
   "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
 XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager",
   "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager");
 
 // List of docShell capabilities to (re)store. These are automatically
@@ -115,24 +123,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
   "resource:///modules/sessionstore/_SessionFile.jsm");
 
 #ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
 #endif
 
+/**
+ * |true| if we are in debug mode, |false| otherwise.
+ * Debug mode is controlled by preference browser.sessionstore.debug
+ */
+let gDebuggingEnabled = false;
 function debug(aMsg) {
-  aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
-  Services.console.logStringMessage(aMsg);
+  if (gDebuggingEnabled) {
+    aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
+    Services.console.logStringMessage(aMsg);
+  }
 }
 
 this.SessionStore = {
   get promiseInitialized() {
-    return SessionStoreInternal.promiseInitialized.promise;
+    return SessionStoreInternal.promiseInitialized;
   },
 
   get canRestoreLastSession() {
     return SessionStoreInternal.canRestoreLastSession;
   },
 
   set canRestoreLastSession(val) {
     SessionStoreInternal.canRestoreLastSession = val;
@@ -320,17 +335,17 @@ let SessionStoreInternal = {
   _lastSessionState: null,
 
   // When starting Firefox with a single private window, this is the place
   // where we keep the session we actually wanted to restore in case the user
   // decides to later open a non-private window as well.
   _deferredInitialState: null,
 
   // A promise resolved once initialization is complete
-  _promiseInitialization: Promise.defer(),
+  _deferredInitialized: Promise.defer(),
 
   // Whether session has been initialized
   _sessionInitialized: false,
 
   // True if session store is disabled by multi-process browsing.
   // See bug 516755.
   _disabledForMultiProcess: false,
 
@@ -343,56 +358,69 @@ let SessionStoreInternal = {
   // 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._promiseInitialization;
+    return this._deferredInitialized.promise;
   },
 
   /* ........ Public Getters .............. */
   get canRestoreLastSession() {
     return this._lastSessionState;
   },
 
   set canRestoreLastSession(val) {
     // Cheat a bit; only allow false.
     if (val)
       return;
     this._lastSessionState = null;
   },
 
-  /* ........ Global Event Handlers .............. */
-
   /**
-   * Initialize the component
+   * Initialize the sessionstore service.
    */
-  initService: function ssi_initService() {
-    if (this._sessionInitialized) {
-      return;
-    }
+  init: function (aWindow) {
+    if (this._initialized) {
+      throw new Error("SessionStore.init() must only be called once!");
+    }
+
+    if (!aWindow) {
+      throw new Error("SessionStore.init() must be called with a valid window.");
+    }
+
     TelemetryTimestamps.add("sessionRestoreInitialized");
     OBSERVING.forEach(function(aTopic) {
       Services.obs.addObserver(this, aTopic, true);
     }, this);
 
     this._initPrefs();
-
+    this._initialized = true;
     this._disabledForMultiProcess = this._prefBranch.getBoolPref("tabs.remote");
 
     // this pref is only read at startup, so no need to observe it
     this._sessionhistory_max_entries =
       this._prefBranch.getIntPref("sessionhistory.max_entries");
 
-    gSessionStartup.onceInitialized.then(
-      this.initSession.bind(this)
-    );
+    // Wait until nsISessionStartup has finished reading the session data.
+    gSessionStartup.onceInitialized.then(() => {
+      // Parse session data and start restoring.
+      this.initSession();
+
+      // Start tracking the given (initial) browser window.
+      if (!aWindow.closed) {
+        this.onLoad(aWindow);
+      }
+
+      // Let everyone know we're done.
+      this._deferredInitialized.resolve();
+    });
   },
 
   initSession: function ssi_initSession() {
     let ss = gSessionStartup;
     try {
       if (ss.doRestore() ||
           ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
         this._initialState = ss.state;
@@ -461,48 +489,27 @@ let SessionStoreInternal = {
           this._initialState.windows.forEach(function(aWindow) {
             delete aWindow.__lastSessionWindowID;
           });
         }
       }
       catch (ex) { debug("The session file is invalid: " + ex); }
     }
 
-    // A Lazy getter for the sessionstore.js backup promise.
-    XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function () {
-      // We're creating a backup of sessionstore.js by moving it to .bak
-      // because that's a lot faster than creating a copy. sessionstore.js
-      // would be overwritten shortly afterwards anyway so we can save time
-      // and just move instead of copy.
-      return _SessionFile.moveToBackupPath();
-    });
-
     // 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();
 
-    // The service is ready. Backup-on-upgrade might still be in progress,
-    // but we do not have a race condition:
-    //
-    // - if the file to backup is named sessionstore.js, secondary
-    // backup will be started in this tick, so any further I/O will be
-    // scheduled to start after the secondary backup is complete;
-    //
-    // - if the file is named sessionstore.bak, it will only be erased
-    // by the getter to |_backupSessionFileOnce|, which specifically
-    // waits until the secondary backup has been completed or deemed
-    // useless before causing any side-effects.
     this._sessionInitialized = true;
-    this._promiseInitialization.resolve();
   },
 
   /**
    * If this is the first time we launc this build of Firefox,
    * backup sessionstore.js.
    */
   _performUpgradeBackup: function ssi_performUpgradeBackup() {
     // Perform upgrade backup, if necessary
@@ -532,19 +539,23 @@ let SessionStoreInternal = {
   _initEncoding : function ssi_initEncoding() {
     // The (UTF-8) encoder used to write to files.
     XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () {
       return new TextEncoder();
     });
   },
 
   _initPrefs : function() {
-    XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () {
-      return Services.prefs.getBranch("browser.");
-    });
+    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");
     });
 
@@ -561,53 +572,29 @@ let SessionStoreInternal = {
     });
 
     XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
       this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
       return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
     });
   },
 
-  _initWindow: function ssi_initWindow(aWindow) {
-    if (aWindow) {
-      this.onLoad(aWindow);
-    } else if (this._loadState == STATE_STOPPED) {
-      // If init is being called with a null window, it's possible that we
-      // just want to tell sessionstore that a session is live (as is the case
-      // with starting Firefox with -private, for example; see bug 568816),
-      // so we should mark the load state as running to make sure that
-      // things like setBrowserState calls will succeed in restoring the session.
-      this._loadState = STATE_RUNNING;
-    }
-  },
-
-  /**
-   * Start tracking a window.
-   *
-   * This function also initializes the component if it is not
-   * initialized yet.
-   */
-  init: function ssi_init(aWindow) {
-    let self = this;
-    this.initService();
-    this._promiseInitialization.promise.then(
-      function onSuccess() {
-        self._initWindow(aWindow);
-      }
-    );
-  },
-
   /**
    * Called on application shutdown, after notifications:
    * quit-application-granted, quit-application
    */
   _uninit: function ssi_uninit() {
+    if (!this._initialized) {
+      throw new Error("SessionStore is not initialized.");
+    }
+
     // save all data for session resuming
-    if (this._sessionInitialized)
+    if (this._sessionInitialized) {
       this.saveState(true);
+    }
 
     // clear out priority queue in case it's still holding refs
     TabRestoreQueue.reset();
 
     // Make sure to break our cycle with the save timer
     if (this._saveTimer) {
       this._saveTimer.cancel();
       this._saveTimer = null;
@@ -665,16 +652,23 @@ let SessionStoreInternal = {
 
     switch (aMessage.name) {
       case "SessionStore:pageshow":
         this.onTabLoad(win, browser);
         break;
       case "SessionStore:input":
         this.onTabInput(win, browser);
         break;
+      case "SessionStore:MozStorageChanged":
+        TabStateCache.delete(browser);
+        this.saveStateDelayed(win);
+        break;
+      case "SessionStore:loadStart":
+        TabStateCache.delete(browser);
+        break;
       default:
         debug("received unknown message '" + aMessage.name + "'");
         break;
     }
 
     this._clearRestoringWindows();
   },
 
@@ -689,16 +683,17 @@ let SessionStoreInternal = {
 
     var win = aEvent.currentTarget.ownerDocument.defaultView;
     switch (aEvent.type) {
       case "load":
         // If __SS_restore_data is set, then we need to restore the document
         // (form data, scrolling, etc.). This will only happen when a tab is
         // first restored.
         let browser = aEvent.currentTarget;
+        TabStateCache.delete(browser);
         if (browser.__SS_restore_data)
           this.restoreDocument(win, browser, aEvent);
         this.onTabLoad(win, browser);
         break;
       case "TabOpen":
         this.onTabAdd(win, aEvent.originalTarget);
         break;
       case "TabClose":
@@ -712,21 +707,26 @@ let SessionStoreInternal = {
         break;
       case "TabShow":
         this.onTabShow(win, aEvent.originalTarget);
         break;
       case "TabHide":
         this.onTabHide(win, aEvent.originalTarget);
         break;
       case "TabPinned":
-      case "TabUnpinned":
+        // If possible, update cached data without having to invalidate it
+        TabStateCache.update(aEvent.originalTarget, "pinned", true);
         this.saveStateDelayed(win);
         break;
-    }
-
+      case "TabUnpinned":
+        // If possible, update cached data without having to invalidate it
+        TabStateCache.update(aEvent.originalTarget, "pinned", false);
+        this.saveStateDelayed(win);
+        break;
+    }
     this._clearRestoringWindows();
   },
 
   /**
    * If it's the first window load since app start...
    * - determine if we're reloading after a crash or a forced-restart
    * - restore window state
    * - restart downloads
@@ -1086,16 +1086,17 @@ let SessionStoreInternal = {
     // session data on disk as this notification fires after the
     // quit-application notification so the browser is about to exit.
     if (this._loadState == STATE_QUITTING)
       return;
     this._lastSessionState = null;
     let openWindows = {};
     this._forEachBrowserWindow(function(aWindow) {
       Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
+        TabStateCache.delete(aTab);
         delete aTab.linkedBrowser.__SS_data;
         delete aTab.linkedBrowser.__SS_tabStillLoading;
         delete aTab.linkedBrowser.__SS_formDataSaved;
         delete aTab.linkedBrowser.__SS_hostSchemeData;
         if (aTab.linkedBrowser.__SS_restoreState)
           this._resetTabRestoringState(aTab);
       });
       openWindows[aWindow.__SSi] = true;
@@ -1309,19 +1310,18 @@ let SessionStoreInternal = {
     event.initEvent("SSTabClosing", true, false);
     aTab.dispatchEvent(event);
 
     // don't update our internal state if we don't have to
     if (this._max_tabs_undo == 0) {
       return;
     }
 
-    // make sure that the tab related data is up-to-date
-    var tabState = this._collectTabData(aTab);
-    this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
+    // Get the latest data for this tab (generally, from the cache)
+    let tabState = this._collectTabData(aTab);
 
     // store closed-tab data for undo
     if (this._shouldSaveTabState(tabState)) {
       let tabTitle = aTab.label;
       let tabbrowser = aWindow.gBrowser;
       tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
 
       this._windows[aWindow.__SSi]._closedTabs.unshift({
@@ -1332,32 +1332,35 @@ let SessionStoreInternal = {
       });
       var length = this._windows[aWindow.__SSi]._closedTabs.length;
       if (length > this._max_tabs_undo)
         this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo);
     }
   },
 
   /**
-   * When a tab loads, save state.
+   * When a tab loads, invalidate its cached state, trigger async save.
+   *
    * @param aWindow
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabLoad: function ssi_onTabLoad(aWindow, aBrowser) {
     // react on "load" and solitary "pageshow" events (the first "pageshow"
     // following "load" is too late for deleting the data caches)
     // It's possible to get a load event after calling stop on a browser (when
     // overwriting tabs). We want to return early if the tab hasn't been restored yet.
     if (aBrowser.__SS_restoreState &&
         aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       return;
     }
 
+    TabStateCache.delete(aBrowser);
+
     delete aBrowser.__SS_data;
     delete aBrowser.__SS_tabStillLoading;
     delete aBrowser.__SS_formDataSaved;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
@@ -1368,16 +1371,18 @@ let SessionStoreInternal = {
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
     // deleting __SS_formDataSaved will cause us to recollect form data
     delete aBrowser.__SS_formDataSaved;
 
+    TabStateCache.delete(aBrowser);
+
     this.saveStateDelayed(aWindow, 3000);
   },
 
   /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
@@ -1404,28 +1409,34 @@ let SessionStoreInternal = {
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.hiddenToVisible(aTab);
 
       // let's kick off tab restoration again to ensure this tab gets restored
       // with "restore_hidden_tabs" == false (now that it has become visible)
       this.restoreNextTab();
     }
 
+    // If possible, update cached data without having to invalidate it
+    TabStateCache.update(aTab, "hidden", false);
+
     // Default delay of 2 seconds gives enough time to catch multiple TabShow
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   onTabHide: function ssi_onTabHide(aWindow, aTab) {
     // If the tab hasn't been restored yet, move it into the right bucket
     if (aTab.linkedBrowser.__SS_restoreState &&
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.visibleToHidden(aTab);
     }
 
+    // If possible, update cached data without having to invalidate it
+    TabStateCache.update(aTab, "hidden", true);
+
     // Default delay of 2 seconds gives enough time to catch multiple TabHide
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   /* ........ nsISessionStore API .............. */
 
   getBrowserState: function ssi_getBrowserState() {
@@ -1491,42 +1502,63 @@ let SessionStoreInternal = {
 
     this.restoreWindow(aWindow, aState, aOverwrite);
   },
 
   getTabState: function ssi_getTabState(aTab) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
-    var tabState = this._collectTabData(aTab);
-
-    var window = aTab.ownerDocument.defaultView;
-    this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState);
+    let tabState = this._collectTabData(aTab);
 
     return this._toJSONString(tabState);
   },
 
   setTabState: function ssi_setTabState(aTab, aState) {
-    var tabState = JSON.parse(aState);
-    if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
+    // Remove the tab state from the cache.
+    // Note that we cannot simply replace the contents of the cache
+    // as |aState| can be an incomplete state that will be completed
+    // by |restoreHistoryPrecursor|.
+    let tabState = JSON.parse(aState);
+    if (!tabState) {
+      debug("Empty state argument");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+    if (typeof tabState != "object") {
+      debug("State argument does not represent an object");
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
-
-    var window = aTab.ownerDocument.defaultView;
+    }
+    if (!("entries" in tabState)) {
+      debug("State argument must contain field 'entries'");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+    if (!aTab.ownerDocument) {
+      debug("Tab argument must have an owner document");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    let window = aTab.ownerDocument.defaultView;
+    if (!("__SSi" in window)) {
+      debug("Default view of ownerDocument must have a unique identifier");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    TabStateCache.delete(aTab);
     this._setWindowStateBusy(window);
     this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0);
   },
 
   duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi ||
         !aWindow.getBrowser)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
-    var tabState = this._collectTabData(aTab, true);
-    var sourceWindow = aTab.ownerDocument.defaultView;
-    this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true);
+    // Duplicate the tab state
+    let tabState = this._cloneFullTabData(aTab);
+
     tabState.index += aDelta;
     tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
     tabState.pinned = false;
 
     this._setWindowStateBusy(aWindow);
     let newTab = aTab == aWindow.gBrowser.selectedTab ?
       aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
       aWindow.gBrowser.addTab();
@@ -1686,31 +1718,33 @@ let SessionStoreInternal = {
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
     }
   },
 
   deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) {
     if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
         this._windows[aWindow.__SSi].extData[aKey])
       delete this._windows[aWindow.__SSi].extData[aKey];
+    this.saveStateDelayed(aWindow);
   },
 
   getTabValue: function ssi_getTabValue(aTab, aKey) {
     let data = {};
     if (aTab.__SS_extdata) {
       data = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       // If the tab hasn't been fully restored, get the data from the to-be-restored data
       data = aTab.linkedBrowser.__SS_data.extData;
     }
     return data[aKey] || "";
   },
 
   setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
+    TabStateCache.delete(aTab);
     // If the tab hasn't been restored, then set the data there, otherwise we
     // could lose newly added data.
     let saveTo;
     if (aTab.__SS_extdata) {
       saveTo = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       saveTo = aTab.linkedBrowser.__SS_data.extData;
@@ -1719,33 +1753,36 @@ let SessionStoreInternal = {
       aTab.__SS_extdata = {};
       saveTo = aTab.__SS_extdata;
     }
     saveTo[aKey] = aStringValue;
     this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
+    TabStateCache.delete(aTab);
     // We want to make sure that if data is accessed early, we attempt to delete
     // that data from __SS_data as well. Otherwise we'll throw in cases where
     // data can be set or read.
     let deleteFrom;
     if (aTab.__SS_extdata) {
       deleteFrom = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       deleteFrom = aTab.linkedBrowser.__SS_data.extData;
     }
 
     if (deleteFrom && deleteFrom[aKey])
       delete deleteFrom[aKey];
+    this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   persistTabAttribute: function ssi_persistTabAttribute(aName) {
     if (TabAttributes.persist(aName)) {
+      TabStateCache.clear();
       this.saveStateDelayed();
     }
   },
 
   /**
    * Restores the session state stored in _lastSessionState. This will attempt
    * to merge data into the current session. If a window was opened at startup
    * with pinned tab(s), then the remaining data from the previous session for
@@ -1909,47 +1946,66 @@ let SessionStoreInternal = {
     }
 
     return [true, canOverwriteTabs];
   },
 
   /* ........ Saving Functionality .............. */
 
   /**
-   * Store all session data for a window
-   * @param aWindow
-   *        Window reference
+   * Collect data related to a single tab
+   *
+   * @param aTab
+   *        tabbrowser tab
+   *
+   * @returns {TabData} An object with the data for this tab.  If the
+   * tab has not been invalidated since the last call to
+   * _collectTabData(aTab), the same object is returned.
    */
-  _saveWindowHistory: function ssi_saveWindowHistory(aWindow) {
-    var tabbrowser = aWindow.gBrowser;
-    var tabs = tabbrowser.tabs;
-    var tabsData = this._windows[aWindow.__SSi].tabs = [];
-
-    for (var i = 0; i < tabs.length; i++)
-      tabsData.push(this._collectTabData(tabs[i]));
-
-    this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1;
+  _collectTabData: function ssi_collectTabData(aTab) {
+    if (!aTab) {
+      throw new TypeError("Expecting a tab");
+    }
+    let tabData;
+    if ((tabData = TabStateCache.get(aTab))) {
+      return tabData;
+    }
+    tabData = new TabData(this._collectBaseTabData(aTab));
+    if (this._updateTextAndScrollDataForTab(aTab, tabData)) {
+      TabStateCache.set(aTab, tabData);
+    }
+    return tabData;
   },
 
   /**
-   * Collect data related to a single tab
+   * Collect data related to a single tab, including private data.
+   * Use with caution.
+   *
    * @param aTab
    *        tabbrowser tab
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
-   * @returns object
+   *
+   * @returns {object} An object with the data for this tab. This object
+   * is recomputed at every call.
    */
-  _collectTabData: function ssi_collectTabData(aTab, aFullData) {
-    var tabData = { entries: [], lastAccessed: aTab.lastAccessed };
-    var browser = aTab.linkedBrowser;
-
-    if (!browser || !browser.currentURI)
+  _cloneFullTabData: function ssi_cloneFullTabData(aTab) {
+    let options = { includePrivateData: true };
+    let tabData = this._collectBaseTabData(aTab, options);
+    this._updateTextAndScrollDataForTab(aTab, tabData, options);
+    return tabData;
+  },
+
+  _collectBaseTabData: function ssi_collectBaseTabData(aTab, aOptions = null) {
+    let includePrivateData = aOptions && aOptions.includePrivateData;
+    let tabData = {entries: [], lastAccessed: aTab.lastAccessed };
+    let browser = aTab.linkedBrowser;
+    if (!browser || !browser.currentURI) {
       // can happen when calling this function right after .addTab()
       return tabData;
-    else if (browser.__SS_data && browser.__SS_tabStillLoading) {
+    }
+    if (browser.__SS_data && browser.__SS_tabStillLoading) {
       // use the data to be restored when the tab hasn't been completely loaded
       tabData = browser.__SS_data;
       if (aTab.pinned)
         tabData.pinned = true;
       else
         delete tabData.pinned;
       tabData.hidden = aTab.hidden;
 
@@ -1969,26 +2025,26 @@ let SessionStoreInternal = {
     }
     catch (ex) { } // this could happen if we catch a tab during (de)initialization
 
     // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse
     //           data even when we shouldn't (e.g. Back, different anchor)
     if (history && browser.__SS_data &&
         browser.__SS_data.entries[history.index] &&
         browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
-        history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
+        history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) {
       tabData = browser.__SS_data;
       tabData.index = history.index + 1;
     }
     else if (history && history.count > 0) {
       browser.__SS_hostSchemeData = [];
       try {
         for (var j = 0; j < history.count; j++) {
           let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
-                                                  aFullData, aTab.pinned, browser.__SS_hostSchemeData);
+                                                  includePrivateData, aTab.pinned, browser.__SS_hostSchemeData);
           tabData.entries.push(entry);
         }
         // If we make it through the for loop, then we're ok and we should clear
         // any indicator of brokenness.
         delete aTab.__SS_broken_history;
       }
       catch (ex) {
         // In some cases, getEntryAtIndex will throw. This seems to be due to
@@ -2004,17 +2060,17 @@ let SessionStoreInternal = {
           NS_ASSERT(false, "SessionStore failed gathering complete history " +
                            "for the focused window/tab. See bug 669196.");
           aTab.__SS_broken_history = true;
         }
       }
       tabData.index = history.index + 1;
 
       // make sure not to cache privacy sensitive data which shouldn't get out
-      if (!aFullData)
+      if (!includePrivateData)
         browser.__SS_data = tabData;
     }
     else if (browser.currentURI.spec != "about:blank" ||
              browser.contentDocument.body.hasChildNodes()) {
       tabData.entries[0] = { url: browser.currentURI.spec };
       tabData.index = 1;
     }
 
@@ -2053,39 +2109,39 @@ let SessionStoreInternal = {
     tabData.image = tabbrowser.getIcon(aTab);
 
     if (aTab.__SS_extdata)
       tabData.extData = aTab.__SS_extdata;
     else if (tabData.extData)
       delete tabData.extData;
 
     if (history && browser.docShell instanceof Ci.nsIDocShell) {
-      let storageData = SessionStorage.serialize(browser.docShell, aFullData)
+      let storageData = SessionStorage.serialize(browser.docShell, includePrivateData)
       if (Object.keys(storageData).length)
         tabData.storage = storageData;
     }
 
     return tabData;
   },
 
   /**
    * Get an object that is a serialized representation of a History entry
    * Used for data storage
    * @param aEntry
    *        nsISHEntry instance
-   * @param aFullData
+   * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    * @param aHostSchemeData
    *        an array of objects with host & scheme keys
    * @returns object
    */
   _serializeHistoryEntry:
-    function ssi_serializeHistoryEntry(aEntry, aFullData, aIsPinned, aHostSchemeData) {
+    function ssi_serializeHistoryEntry(aEntry, aIncludePrivateData, aIsPinned, aHostSchemeData) {
     var entry = { url: aEntry.URI.spec };
 
     try {
       // throwing is expensive, we know that about: pages will throw
       if (entry.url.indexOf("about:") != 0)
         aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme });
     }
     catch (ex) {
@@ -2126,26 +2182,26 @@ let SessionStoreInternal = {
 
     var x = {}, y = {};
     aEntry.getScrollPosition(x, y);
     if (x.value != 0 || y.value != 0)
       entry.scroll = x.value + "," + y.value;
 
     try {
       var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata");
-      if (aEntry.postData && (aFullData || prefPostdata &&
+      if (aEntry.postData && (aIncludePrivateData || prefPostdata &&
             this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) {
         aEntry.postData.QueryInterface(Ci.nsISeekableStream).
                         seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
         var stream = Cc["@mozilla.org/binaryinputstream;1"].
                      createInstance(Ci.nsIBinaryInputStream);
         stream.setInputStream(aEntry.postData);
         var postBytes = stream.readByteArray(stream.available());
         var postdata = String.fromCharCode.apply(null, postBytes);
-        if (aFullData || prefPostdata == -1 ||
+        if (aIncludePrivateData || prefPostdata == -1 ||
             postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <=
               prefPostdata) {
           // We can stop doing base64 encoding once our serialization into JSON
           // is guaranteed to handle all chars in strings, including embedded
           // nulls.
           entry.postdata_b64 = btoa(postdata);
         }
       }
@@ -2196,119 +2252,113 @@ let SessionStoreInternal = {
 
         if (child) {
           // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
           if (child.URI.schemeIs("wyciwyg")) {
             children = [];
             break;
           }
 
-          children.push(this._serializeHistoryEntry(child, aFullData,
+          children.push(this._serializeHistoryEntry(child, aIncludePrivateData,
                                                     aIsPinned, aHostSchemeData));
         }
       }
 
       if (children.length)
         entry.children = children;
     }
 
     return entry;
   },
 
   /**
-   * go through all tabs and store the current scroll positions
+   * Go through all frames and store the current scroll positions
    * and innerHTML content of WYSIWYG editors
-   * @param aWindow
-   *        Window reference
-   */
-  _updateTextAndScrollData: function ssi_updateTextAndScrollData(aWindow) {
-    var browsers = aWindow.gBrowser.browsers;
-    this._windows[aWindow.__SSi].tabs.forEach(function (tabData, i) {
-      try {
-        this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData);
-      }
-      catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time)
-    }, this);
-  },
-
-  /**
-   * go through all frames and store the current scroll positions
-   * and innerHTML content of WYSIWYG editors
-   * @param aWindow
-   *        Window reference
-   * @param aBrowser
-   *        single browser reference
+   *
+   * @param aTab
+   *        tabbrowser tab
    * @param aTabData
    *        tabData object to add the information to
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
+   * @param options
+   *        An optional object that may contain the following field:
+   *        - includePrivateData: always return privacy sensitive data
+   *          (use with care)
+   * @return false if data should not be cached because the tab
+   *        has not been fully initialized yet.
    */
   _updateTextAndScrollDataForTab:
-    function ssi_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) {
+    function ssi_updateTextAndScrollDataForTab(aTab, aTabData, aOptions = null) {
+    let includePrivateData = aOptions && aOptions.includePrivateData;
+    let window = aTab.ownerDocument.defaultView;
+    let browser = aTab.linkedBrowser;
     // we shouldn't update data for incompletely initialized tabs
-    if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading)
-      return;
-
-    var tabIndex = (aTabData.index || aTabData.entries.length) - 1;
+    if (!browser.currentURI
+        || (browser.__SS_data && browser.__SS_tabStillLoading)) {
+      return false;
+    }
+
+    let tabIndex = (aTabData.index || aTabData.entries.length) - 1;
     // entry data needn't exist for tabs just initialized with an incomplete session state
-    if (!aTabData.entries[tabIndex])
-      return;
-
-    let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
-                            this._getSelectedPageStyle(aBrowser.contentWindow);
+    if (!aTabData.entries[tabIndex]) {
+      return false;
+    }
+
+    let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
+                            this._getSelectedPageStyle(browser.contentWindow);
     if (selectedPageStyle)
       aTabData.pageStyle = selectedPageStyle;
     else if (aTabData.pageStyle)
       delete aTabData.pageStyle;
 
-    this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
+    this._updateTextAndScrollDataForFrame(window, browser.contentWindow,
                                           aTabData.entries[tabIndex],
-                                          !aBrowser.__SS_formDataSaved, aFullData,
+                                          !browser.__SS_formDataSaved, includePrivateData,
                                           !!aTabData.pinned);
-    aBrowser.__SS_formDataSaved = true;
-    if (aBrowser.currentURI.spec == "about:config")
+    browser.__SS_formDataSaved = true;
+    if (browser.currentURI.spec == "about:config")
       aTabData.entries[tabIndex].formdata = {
         id: {
-          "textbox": aBrowser.contentDocument.getElementById("textbox").value
+          "textbox": browser.contentDocument.getElementById("textbox").value
         },
         xpath: {}
       };
+      return true;
   },
 
   /**
    * go through all subframes and store all form data, the current
    * scroll positions and innerHTML content of WYSIWYG editors
    * @param aWindow
    *        Window reference
    * @param aContent
    *        frame reference
    * @param aData
    *        part of a tabData object to add the information to
    * @param aUpdateFormData
    *        update all form data for this tab
-   * @param aFullData
+   * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    */
   _updateTextAndScrollDataForFrame:
     function ssi_updateTextAndScrollDataForFrame(aWindow, aContent, aData,
-                                                 aUpdateFormData, aFullData, aIsPinned) {
+                                                 aUpdateFormData, aIncludePrivateData, aIsPinned) {
     for (var i = 0; i < aContent.frames.length; i++) {
       if (aData.children && aData.children[i])
         this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
                                               aData.children[i], aUpdateFormData,
-                                              aFullData, aIsPinned);
+                                              aIncludePrivateData, aIsPinned);
     }
     var isHTTPS = this._getURIFromString((aContent.parent || aContent).
                                          document.location.href).schemeIs("https");
     let topURL = aContent.top.document.location.href;
     let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback";
-    if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
-      if (aFullData || aUpdateFormData) {
+    if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
+      if (aIncludePrivateData || aUpdateFormData) {
         let formData = DocumentUtils.getFormData(aContent.document);
 
         // We want to avoid saving data for about:sessionrestore as a string.
         // Since it's stored in the form as stringified JSON, stringifying further
         // causes an explosion of escape characters. cf. bug 467409
         if (formData && isAboutSR) {
           formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
         }
@@ -2422,38 +2472,16 @@ let SessionStoreInternal = {
       aHosts[aHost] = aIsPinned;
     }
     else if (aScheme == "file") {
       aHosts[aHost] = true;
     }
   },
 
   /**
-   * store all hosts for a URL
-   * @param aWindow
-   *        Window reference
-   */
-  _updateCookieHosts: function ssi_updateCookieHosts(aWindow) {
-    var hosts = this._internalWindows[aWindow.__SSi].hosts = {};
-
-    // Since _updateCookiesHosts is only ever called for open windows during a
-    // session, we can call into _extractHostsForCookiesFromHostScheme directly
-    // using data that is attached to each browser.
-    for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) {
-      let tab = aWindow.gBrowser.tabs[i];
-      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
-      for (let j = 0; j < hostSchemeData.length; j++) {
-        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
-                                                   hostSchemeData[j].scheme,
-                                                   hosts, true, tab.pinned);
-      }
-    }
-  },
-
-  /**
    * Serialize cookie data
    * @param aWindows
    *        JS object containing window data references
    *        { id: winData, etc. }
    */
   _updateCookies: function ssi_updateCookies(aWindows) {
     function addCookieToHash(aHash, aHost, aPath, aName, aCookie) {
       // lazily build up a 3-dimensional hash, with
@@ -2680,20 +2708,39 @@ let SessionStoreInternal = {
 
     return { windows: [winData] };
   },
 
   _collectWindowData: function ssi_collectWindowData(aWindow) {
     if (!this._isWindowLoaded(aWindow))
       return;
 
+    let tabbrowser = aWindow.gBrowser;
+    let tabs = tabbrowser.tabs;
+    let winData = this._windows[aWindow.__SSi];
+    let tabsData = winData.tabs = [];
+    let hosts = this._internalWindows[aWindow.__SSi].hosts = {};
+
     // update the internal state data for this window
-    this._saveWindowHistory(aWindow);
-    this._updateTextAndScrollData(aWindow);
-    this._updateCookieHosts(aWindow);
+    for (let tab of tabs) {
+      tabsData.push(this._collectTabData(tab));
+
+      // Since we are only ever called for open
+      // windows during a session, we can call into
+      // _extractHostsForCookiesFromHostScheme directly using data
+      // that is attached to each browser.
+      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
+      for (let j = 0; j < hostSchemeData.length; j++) {
+        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
+                                                   hostSchemeData[j].scheme,
+                                                   hosts, true, tab.pinned);
+      }
+    }
+    winData.selected = tabbrowser.mTabBox.selectedIndex + 1;
+
     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;
 
@@ -2817,20 +2864,23 @@ let SessionStoreInternal = {
       winData.tabs[0].hidden = false;
       tabbrowser.showTab(tabs[0]);
     }
 
     // If overwriting tabs, we want to reset each tab's "restoring" state. Since
     // we're overwriting those tabs, they should no longer be restoring. The
     // tabs will be rebuilt and marked if they need to be restored after loading
     // state (in restoreHistoryPrecursor).
+    // We also want to invalidate any cached information on the tab state.
     if (aOverwriteTabs) {
       for (let i = 0; i < tabbrowser.tabs.length; i++) {
+        let tab = tabbrowser.tabs[i];
+        TabStateCache.delete(tab);
         if (tabbrowser.browsers[i].__SS_restoreState)
-          this._resetTabRestoringState(tabbrowser.tabs[i]);
+          this._resetTabRestoringState(tab);
       }
     }
 
     // We want to set up a counter on the window that indicates how many tabs
     // in this window are unrestored. This will be used in restoreNextTab to
     // determine if gRestoreTabsProgressListener should be removed from the window.
     // If we aren't overwriting existing tabs, then we want to add to the existing
     // count in case there are still tabs restoring.
@@ -2982,32 +3032,33 @@ let SessionStoreInternal = {
    *        Counter for number of times delaying b/c browser or history aren't ready
    * @param aRestoreImmediately
    *        Flag to indicate whether the given set of tabs aTabs should be
    *        restored/loaded immediately even if restore_on_demand = true
    */
   restoreHistoryPrecursor:
     function ssi_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
                                          aIx, aCount, aRestoreImmediately = false) {
+
     var tabbrowser = aWindow.gBrowser;
 
     // make sure that all browsers and their histories are available
     // - if one's not, resume this check in 100ms (repeat at most 10 times)
     for (var t = aIx; t < aTabs.length; t++) {
       try {
         if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) {
           throw new Error();
         }
       }
       catch (ex) { // in case browser or history aren't ready yet
         if (aCount < 10) {
           var restoreHistoryFunc = function(self) {
             self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
                                          aIx, aCount + 1, aRestoreImmediately);
-          }
+          };
           aWindow.setTimeout(restoreHistoryFunc, 100, this);
           return;
         }
       }
     }
 
     if (!this._isWindowLoaded(aWindow)) {
       // from now on, the data will come from the actual window
@@ -3133,17 +3184,16 @@ let SessionStoreInternal = {
       // At this point we're essentially ready for consumers to read/write data
       // via the sessionstore API so we'll send the SSWindowStateReady event.
       this._setWindowStateReady(aWindow);
       return; // no more tabs to restore
     }
 
     var tab = aTabs.shift();
     var tabData = aTabData.shift();
-
     var browser = aWindow.gBrowser.getBrowserForTab(tab);
     var history = browser.webNavigation.sessionHistory;
 
     if (history.count > 0) {
       history.PurgeHistory(history.count);
     }
     history.QueryInterface(Ci.nsISHistoryInternal);
 
@@ -3698,27 +3748,27 @@ let SessionStoreInternal = {
   /**
    * save state delayed by N ms
    * 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, aDelay) {
+  saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) {
     if (aWindow) {
       this._dirtyWindows[aWindow.__SSi] = true;
     }
 
     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 || 2000);
+      aDelay = Math.max(minimalDelay, aDelay);
       if (aDelay > 0) {
         this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
       }
       else {
         this.saveState();
       }
     }
@@ -3824,35 +3874,19 @@ let SessionStoreInternal = {
     Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
     data = stateString.data;
 
     // Don't touch the file if an observer has deleted all state data.
     if (!data) {
       return;
     }
 
-    let promise;
-    // If "sessionstore.resume_from_crash" is true, attempt to backup the
-    // session file first, before writing to it.
-    if (this._resume_from_crash) {
-      // Note that we do not have race conditions here as _SessionFile
-      // guarantees that any I/O operation is completed before proceeding to
-      // the next I/O operation.
-      // Note backup happens only once, on initial save.
-      promise = this._backupSessionFileOnce;
-    } else {
-      promise = Promise.resolve();
-    }
-
-    // Attempt to write to the session file (potentially, depending on
-    // "sessionstore.resume_from_crash" preference, after successful backup).
-    promise = promise.then(function onSuccess() {
-      // Write (atomically) to a session file, using a tmp file.
-      return _SessionFile.write(data);
-    });
+    // Write (atomically) to a session file, using a tmp file.
+    let promise =
+      _SessionFile.write(data, {backupOnFirstWrite: this._resume_from_crash});
 
     // 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",
         "");
     });
@@ -3980,16 +4014,17 @@ let SessionStoreInternal = {
    */
   _getTabForBrowser: function ssi_getTabForBrowser(aBrowser) {
     let window = aBrowser.ownerDocument.defaultView;
     for (let i = 0; i < window.gBrowser.tabs.length; i++) {
       let tab = window.gBrowser.tabs[i];
       if (tab.linkedBrowser == aBrowser)
         return tab;
     }
+    return undefined;
   },
 
   /**
    * Whether or not to resume session, if not recovering from a crash.
    * @returns bool
    */
   _doResumeSession: function ssi_doResumeSession() {
     return this._prefBranch.getIntPref("startup.page") == 3 ||
@@ -4802,21 +4837,35 @@ function SessionStoreSHistoryListener(aT
   this.tab = aTab;
 }
 SessionStoreSHistoryListener.prototype = {
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsISHistoryListener,
     Ci.nsISupportsWeakReference
   ]),
   browser: null,
-  OnHistoryNewEntry: function(aNewURI) { },
-  OnHistoryGoBack: function(aBackURI) { return true; },
-  OnHistoryGoForward: function(aForwardURI) { return true; },
-  OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; },
-  OnHistoryPurge: function(aNumEntries) { return true; },
+// The following events (with the exception of OnHistoryPurge)
+// accompany either a "load" or a "pageshow" which will in turn cause
+// invalidations.
+  OnHistoryNewEntry: function(aNewURI) {
+
+  },
+  OnHistoryGoBack: function(aBackURI) {
+    return true;
+  },
+  OnHistoryGoForward: function(aForwardURI) {
+    return true;
+  },
+  OnHistoryGotoIndex: function(aIndex, aGotoURI) {
+    return true;
+  },
+  OnHistoryPurge: function(aNumEntries) {
+    TabStateCache.delete(this.tab);
+    return true;
+  },
   OnHistoryReload: function(aReloadURI, aReloadFlags) {
     // On reload, we want to make sure that session history loads the right
     // URI. In order to do that, we will juet call restoreTab. That will remove
     // the history listener and load the right URI.
     SessionStoreInternal.restoreTab(this.tab);
     // Returning false will stop the load that docshell is attempting.
     return false;
   }
@@ -4829,9 +4878,111 @@ String.prototype.hasRootDomain = functio
     return false;
 
   if (this == aDomain)
     return true;
 
   let prevChar = this[index - 1];
   return (index == (this.length - aDomain.length)) &&
          (prevChar == "." || prevChar == "/");
+};
+
+function TabData(obj = null) {
+  if (obj) {
+    if (obj instanceof TabData) {
+      // FIXME: Can we get rid of this?
+      return obj;
+    }
+    for (let [key, value] in Iterator(obj)) {
+      this[key] = value;
+    }
+  }
+  return this;
 }
+
+/**
+ * A cache for tabs data.
+ *
+ * This cache implements a weak map from tabs (as XUL elements)
+ * to tab data (as instances of TabData).
+ *
+ * Note that we should never cache private data, as:
+ * - that data is used very seldom by SessionStore;
+ * - caching private data in addition to public data is memory consuming.
+ */
+let TabStateCache = {
+  _data: new WeakMap(),
+
+  /**
+   * Add or replace an entry in the cache.
+   *
+   * @param {XULElement} aTab The key, which may be either a tab
+   * or the corresponding browser. The binding will disappear
+   * if the tab/browser is destroyed.
+   * @param {TabData} aValue The data associated to |aTab|.
+   */
+  set: function(aTab, aValue) {
+    let key = this._normalizeToBrowser(aTab);
+    if (!(aValue instanceof TabData)) {
+      throw new TypeError("Attempting to cache a non TabData");
+    }
+    this._data.set(key, aValue);
+  },
+
+  /**
+   * Return the tab data associated with a tab.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   *
+   * @return {TabData|undefined} The data if available, |undefined|
+   * otherwise.
+   */
+  get: function(aKey) {
+    let key = this._normalizeToBrowser(aKey);
+    return this._data.get(key);
+  },
+
+  /**
+   * Delete the tab data associated with a tab.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   *
+   * Noop of there is no tab data associated with the tab.
+   */
+  delete: function(aKey) {
+    let key = this._normalizeToBrowser(aKey);
+    this._data.delete(key);
+  },
+
+  /**
+   * Delete all tab data.
+   */
+  clear: function() {
+    this._data.clear();
+  },
+
+  /**
+   * Update in place a piece of data.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   * If the tab/browser is not present, do nothing.
+   * @param {string} aField The field to update.
+   * @param {*} aValue The new value to place in the field.
+   */
+  update: function(aKey, aField, aValue) {
+    let key = this._normalizeToBrowser(aKey);
+    let data = this._data.get(key);
+    if (data) {
+      data[aField] = aValue;
+    }
+  },
+
+  _normalizeToBrowser: function(aKey) {
+    let nodeName = aKey.localName;
+    if (nodeName == "tab") {
+      return aKey.linkedBrowser;
+    }
+    if (nodeName == "browser") {
+      return aKey;
+    }
+    throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
+  }
+};
--- a/browser/components/sessionstore/src/SessionWorker.js
+++ b/browser/components/sessionstore/src/SessionWorker.js
@@ -52,16 +52,22 @@ self.onmessage = function (msg) {
 let Agent = {
   // The initial session string as read from disk.
   initialState: null,
 
   // Boolean that tells whether we already wrote
   // the loadState to disk once after startup.
   hasWrittenLoadStateOnce: false,
 
+  // Boolean that tells whether we already made a
+  // call to write(). We will only attempt to move
+  // sessionstore.js to sessionstore.bak on the
+  // first write.
+  hasWrittenState: false,
+
   // The path to sessionstore.js
   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
 
   // The path to sessionstore.bak
   backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
 
   /**
    * This method is only intended to be called by _SessionFile.syncRead() and
@@ -102,17 +108,29 @@ let Agent = {
 
     // No sessionstore data files found. Return an empty string.
     return "";
   },
 
   /**
    * Write the session to disk.
    */
-  write: function (stateString) {
+  write: function (stateString, options) {
+    if (!this.hasWrittenState) {
+      if (options && options.backupOnFirstWrite) {
+        try {
+          File.move(this.path, this.backupPath);
+        } catch (ex if isNoSuchFileEx(ex)) {
+          // Ignore exceptions about non-existent files.
+        }
+      }
+
+      this.hasWrittenState = true;
+    }
+
     let bytes = Encoder.encode(stateString);
     return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
   },
 
   /**
    * 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.
@@ -135,29 +153,18 @@ let Agent = {
     try {
       state = JSON.parse(this.initialState);
     } finally {
       this.initialState = null;
     }
 
     state.session = state.session || {};
     state.session.state = loadState;
-    return this.write(JSON.stringify(state));
-  },
-
-  /**
-   * Moves sessionstore.js to sessionstore.bak.
-   */
-  moveToBackupPath: function () {
-    try {
-      return File.move(this.path, this.backupPath);
-    } catch (ex if isNoSuchFileEx(ex)) {
-      // Ignore exceptions about non-existent files.
-      return true;
-    }
+    let bytes = Encoder.encode(JSON.stringify(state));
+    return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
   },
 
   /**
    * Creates a copy of sessionstore.js.
    */
   createBackupCopy: function (ext) {
     try {
       return File.copy(this.path, this.backupPath + ext);
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -28,17 +28,17 @@ this.EXPORTED_SYMBOLS = ["_SessionFile"]
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
   "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
@@ -62,34 +62,28 @@ 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) {
-    return SessionFileInternal.write(aData);
+  write: function (aData, aOptions = {}) {
+    return SessionFileInternal.write(aData, aOptions);
   },
   /**
    * 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);
   },
   /**
    * Create a backup copy, asynchronously.
-   */
-  moveToBackupPath: function () {
-    return SessionFileInternal.moveToBackupPath();
-  },
-  /**
-   * Create a backup copy, asynchronously.
    * This is designed to perform backup on upgrade.
    */
   createBackupCopy: function (ext) {
     return SessionFileInternal.createBackupCopy(ext);
   },
   /**
    * Remove a backup copy, asynchronously.
    * This is designed to clean up a backup on upgrade.
@@ -207,24 +201,24 @@ let SessionFileInternal = {
     SessionWorker.post("setInitialState", [text]);
     return text;
   },
 
   read: function () {
     return SessionWorker.post("read").then(msg => msg.ok);
   },
 
-  write: function (aData) {
+  write: function (aData, aOptions) {
     let refObj = {};
     return TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       try {
-        let promise = SessionWorker.post("write", [aData]);
+        let promise = SessionWorker.post("write", [aData, aOptions]);
         // 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 measure how long we had to wait for the result
         yield promise;
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       } catch (ex) {
         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
@@ -234,20 +228,16 @@ let SessionFileInternal = {
       }
     }.bind(this));
   },
 
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     return SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]);
   },
 
-  moveToBackupPath: function () {
-    return SessionWorker.post("moveToBackupPath");
-  },
-
   createBackupCopy: function (ext) {
     return SessionWorker.post("createBackupCopy", [ext]);
   },
 
   removeBackupCopy: function (ext) {
     return SessionWorker.post("removeBackupCopy", [ext]);
   },
 
--- a/browser/components/sessionstore/src/nsSessionStartup.js
+++ b/browser/components/sessionstore/src/nsSessionStartup.js
@@ -34,17 +34,17 @@
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
   "resource:///modules/sessionstore/_SessionFile.jsm");
 
 const STATE_RUNNING_STR = "running";
 
 function debug(aMsg) {
   aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
@@ -157,25 +157,16 @@ SessionStartup.prototype = {
         this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
       else if (!lastSessionCrashed && doResumeSession)
         this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
       else if (this._initialState)
         this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
       else
         this._initialState = null; // reset the state
 
-      // wait for the first browser window to open
-      // Don't reset the initial window's default args (i.e. the home page(s))
-      // if all stored tabs are pinned.
-      if (this.doRestore() &&
-          (!this._initialState.windows ||
-           !this._initialState.windows.every(function (win)
-             win.tabs.every(function (tab) tab.pinned))))
-        Services.obs.addObserver(this, "domwindowopened", true);
-
       Services.obs.addObserver(this, "sessionstore-windows-restored", true);
 
       if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
         Services.obs.addObserver(this, "browser:purge-session-history", true);
 
     } finally {
       // We're ready. Notify everyone else.
       Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
@@ -199,99 +190,86 @@ SessionStartup.prototype = {
       break;
     case "quit-application":
       // no reason for initializing at this point (cf. bug 409115)
       Services.obs.removeObserver(this, "final-ui-startup");
       Services.obs.removeObserver(this, "quit-application");
       if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
         Services.obs.removeObserver(this, "browser:purge-session-history");
       break;
-    case "domwindowopened":
-      var window = aSubject;
-      var self = this;
-      window.addEventListener("load", function() {
-        self._onWindowOpened(window);
-        window.removeEventListener("load", arguments.callee, false);
-      }, false);
-      break;
     case "sessionstore-windows-restored":
       Services.obs.removeObserver(this, "sessionstore-windows-restored");
       // free _initialState after nsSessionStore is done with it
       this._initialState = null;
       break;
     case "browser:purge-session-history":
       Services.obs.removeObserver(this, "browser:purge-session-history");
       // reset all state on sanitization
       this._sessionType = Ci.nsISessionStartup.NO_SESSION;
       break;
     }
   },
 
-  /**
-   * Removes the default arguments from the first browser window
-   * (and removes the "domwindowopened" observer afterwards).
-   */
-  _onWindowOpened: function sss_onWindowOpened(aWindow) {
-    var wType = aWindow.document.documentElement.getAttribute("windowtype");
-    if (wType != "navigator:browser")
-      return;
-
-    /**
-     * Note: this relies on the fact that nsBrowserContentHandler will return
-     * a different value the first time its getter is called after an update,
-     * due to its needHomePageOverride() logic. We don't want to remove the
-     * default arguments in the update case, since they include the "What's
-     * New" page.
-     *
-     * Since we're garanteed to be at least the second caller of defaultArgs
-     * (nsBrowserContentHandler calls it to determine which arguments to pass
-     * at startup), we know that if the window's arguments don't match the
-     * current defaultArguments, we're either in the update case, or we're
-     * launching a non-default browser window, so we shouldn't remove the
-     * window's arguments.
-     */
-    var defaultArgs = Cc["@mozilla.org/browser/clh;1"].
-                      getService(Ci.nsIBrowserHandler).defaultArgs;
-    if (aWindow.arguments && aWindow.arguments[0] &&
-        aWindow.arguments[0] == defaultArgs)
-      aWindow.arguments[0] = null;
-
-    try {
-      Services.obs.removeObserver(this, "domwindowopened");
-    } catch (e) {
-      // This might throw if we're removing the observer multiple times,
-      // but this is safe to ignore.
-    }
-  },
-
 /* ........ Public API ................*/
 
   get onceInitialized() {
     return gOnceInitializedDeferred.promise;
   },
 
   /**
    * Get the session state as a jsval
    */
   get state() {
     this._ensureInitialized();
     return this._initialState;
   },
 
   /**
-   * Determine whether there is a pending session restore.
+   * Determines whether there is a pending session restore and makes sure that
+   * we're initialized before returning. If we're not yet this will read the
+   * session file synchronously.
    * @returns bool
    */
   doRestore: function sss_doRestore() {
     this._ensureInitialized();
+    return this._willRestore();
+  },
+
+  /**
+   * Determines whether there is a pending session restore.
+   * @returns bool
+   */
+  _willRestore: function () {
     return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
            this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
   },
 
   /**
+   * Returns whether we will restore a session that ends up replacing the
+   * homepage. The browser uses this to not start loading the homepage if
+   * we're going to stop its load anyway shortly after.
+   *
+   * This is meant to be an optimization for the average case that loading the
+   * session file finishes before we may want to start loading the default
+   * homepage. Should this be called before the session file has been read it
+   * will just return false.
+   *
+   * @returns bool
+   */
+  get willOverrideHomepage() {
+    if (this._initialState && this._willRestore()) {
+      let windows = this._initialState.windows || null;
+      // If there are valid windows with not only pinned tabs, signal that we
+      // will override the default homepage by restoring a session.
+      return windows && windows.some(w => w.tabs.some(t => !t.pinned));
+    }
+    return false;
+  },
+
+  /**
    * Get the type of pending session store, if any.
    */
   get sessionType() {
     this._ensureInitialized();
     return this._sessionType;
   },
 
   // Ensure that initialization is complete.
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -22,16 +22,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_dying_cache.js \
 	browser_form_restore_events.js \
 	browser_form_restore_events_sample.html \
 	browser_formdata_format.js \
 	browser_formdata_format_sample.html \
 	browser_input.js \
 	browser_input_sample.html \
 	browser_pageshow.js \
+	browser_sessionStorage.js \
         browser_upgrade_backup.js \
 	browser_windowRestore_perwindowpb.js \
 	browser_248970_b_perwindowpb.js \
 	browser_248970_b_sample.html \
 	browser_339445.js \
 	browser_339445_sample.html \
 	browser_345898.js \
 	browser_346337.js \
--- a/browser/components/sessionstore/test/browser_625257.js
+++ b/browser/components/sessionstore/test/browser_625257.js
@@ -1,85 +1,86 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+let Scope = {};
+Cu.import("resource://gre/modules/Task.jsm", Scope);
+Cu.import("resource://gre/modules/Promise.jsm", Scope);
+let {Task, Promise} = Scope;
+
+
 // This tests that a tab which is closed while loading is not lost.
 // Specifically, that session store does not rely on an invalid cache when
 // constructing data for a tab which is loading.
 
-// The newly created tab which we load a URL into and try closing/undoing.
-let tab;
-
 // This test steps through the following parts:
 //  1. Tab has been created is loading URI_TO_LOAD.
 //  2. Before URI_TO_LOAD finishes loading, browser.currentURI has changed and
 //     tab is scheduled to be removed.
 //  3. After the tab has been closed, undoCloseTab() has been called and the tab
 //     should fully load.
 const URI_TO_LOAD = "about:mozilla";
 
+function waitForLoadStarted(aTab) {
+  let deferred = Promise.defer();
+  waitForContentMessage(aTab.linkedBrowser,
+    "SessionStore:loadStart",
+    1000,
+    deferred.resolve);
+  return deferred.promise;
+}
+
+function waitForTabLoaded(aTab) {
+  let deferred = Promise.defer();
+  whenBrowserLoaded(aTab.linkedBrowser, deferred.resolve);
+  return deferred.promise;
+}
+
+function waitForTabClosed() {
+  let deferred = Promise.defer();
+  let observer = function() {
+    gBrowser.tabContainer.removeEventListener("TabClose", observer, true);
+    deferred.resolve();
+  };
+  gBrowser.tabContainer.addEventListener("TabClose", observer, true);
+  return deferred.promise;
+}
+
 function test() {
   waitForExplicitFinish();
 
-  gBrowser.addTabsProgressListener(tabsListener);
-
-  tab = gBrowser.addTab();
-
-  tab.linkedBrowser.addEventListener("load", firstOnLoad, true);
-
-  gBrowser.tabContainer.addEventListener("TabClose", onTabClose, true);
-}
-
-function firstOnLoad(aEvent) {
-  tab.linkedBrowser.removeEventListener("load", firstOnLoad, true);
+  Task.spawn(function() {
+    try {
+      // Open a new tab
+      let tab = gBrowser.addTab("about:blank");
+      yield waitForTabLoaded(tab);
 
-  let uri = aEvent.target.location;
-  is(uri, "about:blank", "first load should be for about:blank");
-
-  // Trigger a save state.
-  ss.getBrowserState();
+      // Trigger a save state, to initialize any caches
+      ss.getBrowserState();
 
-  is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
-  ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
+      is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
+      ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
 
-  tab.linkedBrowser.loadURI(URI_TO_LOAD);
-}
+      // Start a load and interrupt it by closing the tab
+      tab.linkedBrowser.loadURI(URI_TO_LOAD);
+      let loaded = yield waitForLoadStarted(tab);
+      ok(loaded, "Load started");
 
-let tabsListener = {
-  onLocationChange: function onLocationChange(aBrowser) {
-    gBrowser.removeTabsProgressListener(tabsListener);
-
-    is(aBrowser.currentURI.spec, URI_TO_LOAD,
-       "should occur after about:blank load and be loading next page");
-
-    // Since we are running in the context of tabs listeners, we do not
-    // want to disrupt other tabs listeners.
-    executeSoon(function() {
+      let tabClosing = waitForTabClosed();
       gBrowser.removeTab(tab);
-    });
-  }
-};
+      info("Now waiting for TabClose to close");
+      yield tabClosing;
 
-function onTabClose(aEvent) {
-  gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, true);
+      // Undo the tab, ensure that it proceeds with loading
+      tab = ss.undoCloseTab(window, 0);
+      yield waitForTabLoaded(tab);
+      is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD, "loading proceeded as expected");
 
-  is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD,
-     "should only remove when loading page");
+      gBrowser.removeTab(tab);
 
-  executeSoon(function() {
-    tab = ss.undoCloseTab(window, 0);
-    tab.linkedBrowser.addEventListener("load", secondOnLoad, true);
+      executeSoon(finish);
+    } catch (ex) {
+      ok(false, ex);
+      info(ex.stack);
+    }
   });
 }
-
-function secondOnLoad(aEvent) {
-  let uri = aEvent.target.location;
-  is(uri, URI_TO_LOAD, "should load page from undoCloseTab");
-  done();
-}
-
-function done() {
-  tab.linkedBrowser.removeEventListener("load", secondOnLoad, true);
-  gBrowser.removeTab(tab);
-
-  executeSoon(finish);
-}
--- a/browser/components/sessionstore/test/browser_833286_atomic_backup.js
+++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // This tests are for a sessionstore.js atomic backup.
+// Each test will wait for a write to the Session Store
+// before executing.
 
 let tmp = {};
 Cu.import("resource://gre/modules/osfile.jsm", tmp);
 Cu.import("resource://gre/modules/Task.jsm", tmp);
 Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", tmp);
 
 const {OS, Task, _SessionFile} = tmp;
 
@@ -18,17 +20,17 @@ const backupPath = OS.Path.join(OS.Const
 
 // A text decoder.
 let gDecoder = new TextDecoder();
 // Global variables that contain sessionstore.js and sessionstore.bak data for
 // comparison between tests.
 let gSSData;
 let gSSBakData;
 
-// waitForSaveStateComplete waits for a state write completion.
+// Wait for a state write to complete and then execute a callback.
 function waitForSaveStateComplete(aSaveStateCallback) {
   let topic = "sessionstore-state-write-complete";
 
   function observer() {
     Services.prefs.clearUserPref(PREF_SS_INTERVAL);
     Services.obs.removeObserver(observer, topic);
     executeSoon(function taskCallback() {
       Task.spawn(aSaveStateCallback);
@@ -36,61 +38,56 @@ function waitForSaveStateComplete(aSaveS
   }
 
   Services.obs.addObserver(observer, topic, false);
 }
 
 // Register next test callback and trigger state saving change.
 function nextTest(testFunc) {
   waitForSaveStateComplete(testFunc);
+
+  // We set the interval for session store state saves to be zero
+  // to cause a save ASAP.
   Services.prefs.setIntPref(PREF_SS_INTERVAL, 0);
 }
 
 registerCleanupFunction(function() {
   // Cleaning up after the test: removing the sessionstore.bak file.
   Task.spawn(function cleanupTask() {
     yield OS.File.remove(backupPath);
   });
 });
 
 function test() {
   waitForExplicitFinish();
-  nextTest(testInitialWriteNoBackup);
+  nextTest(testAfterFirstWrite);
 }
 
-function testInitialWriteNoBackup() {
-  // Ensure that sessionstore.js is created, but not sessionstore.bak.
-  let ssExists = yield OS.File.exists(path);
-  let ssBackupExists = yield OS.File.exists(backupPath);
-  ok(ssExists, "sessionstore.js should be created.");
-  ok(!ssBackupExists, "sessionstore.bak should not have been created, yet.");
-
-  nextTest(testWriteNoBackup);
-}
-
-function testWriteNoBackup() {
-  // Ensure sessionstore.bak is not created.
+function testAfterFirstWrite() {
+  // Ensure sessionstore.bak is not created. We start with a clean
+  // profile so there was nothing to move to sessionstore.bak before
+  // initially writing sessionstore.js
   let ssExists = yield OS.File.exists(path);
   let ssBackupExists = yield OS.File.exists(backupPath);
   ok(ssExists, "sessionstore.js should exist.");
   ok(!ssBackupExists, "sessionstore.bak should not have been created, yet");
 
   // Save sessionstore.js data to compare to the sessionstore.bak data in the
   // next test.
   let array = yield OS.File.read(path);
   gSSData = gDecoder.decode(array);
 
-  // Manually trigger _SessionFile.moveToBackupPath since the backup once
-  // promise is already resolved and backup would not be triggered again.
-  yield _SessionFile.moveToBackupPath();
+  // Manually move to the backup since the first write has already happened
+  // and a backup would not be triggered again.
+  yield OS.File.move(path, backupPath);
 
-  nextTest(testWriteBackup);
+  nextTest(testReadBackup);
 }
 
-function testWriteBackup() {
+function testReadBackup() {
   // Ensure sessionstore.bak is finally created.
   let ssExists = yield OS.File.exists(path);
   let ssBackupExists = yield OS.File.exists(backupPath);
   ok(ssExists, "sessionstore.js exists.");
   ok(ssBackupExists, "sessionstore.bak should now be created.");
 
   // Read sessionstore.bak data.
   let array = yield OS.File.read(backupPath);
@@ -122,20 +119,21 @@ function testWriteBackup() {
   ssDataRead = yield _SessionFile.read();
   is(ssDataRead, gSSBakData,
     "_SessionFile.read read sessionstore.bak correctly.");
 
   // Read sessionstore.bak with _SessionFile.syncRead.
   ssDataRead = _SessionFile.syncRead();
   is(ssDataRead, gSSBakData,
     "_SessionFile.syncRead read sessionstore.bak correctly.");
-  nextTest(testNoWriteBackup);
+
+  nextTest(testBackupUnchanged);
 }
 
-function testNoWriteBackup() {
+function testBackupUnchanged() {
   // Ensure sessionstore.bak is backed up only once.
 
   // Read sessionstore.bak data.
   let array = yield OS.File.read(backupPath);
   let ssBakData = gDecoder.decode(array);
   // Ensure the sessionstore.bak did not change.
   is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
 
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -25,16 +25,21 @@ function runTests() {
   let state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed by default");
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Flip a couple of allow* flags.
   docShell.allowImages = false;
   docShell.allowMetaRedirects = false;
 
+  // Now reload the document to ensure that these capabilities
+  // are taken into account
+  browser.reload();
+  yield whenBrowserLoaded(browser);
+
   // Check that we correctly save disallowed features.
   let disallowedState = JSON.parse(ss.getTabState(tab));
   let disallow = new Set(disallowedState.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Reuse the tab to restore a new, clean state into it.
@@ -47,17 +52,17 @@ function runTests() {
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Restore the state with disallowed features.
   ss.setTabState(tab, JSON.stringify(disallowedState));
   yield waitForLoad(browser);
 
   // Check that docShell flags are set.
   ok(!docShell.allowImages, "images not allowed");
-  ok(!docShell.allowMetaRedirects, "meta redirects not allowed")
+  ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
 
   // Check that we correctly restored features as disabled.
   state = JSON.parse(ss.getTabState(tab));
   disallow = new Set(state.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed anymore");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
   is(disallow.size, 2, "two capabilities disallowed");
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Scope = {};
+Cu.import("resource://gre/modules/Task.jsm", Scope);
+Cu.import("resource://gre/modules/Promise.jsm", Scope);
+let {Task, Promise} = Scope;
+
+function promiseBrowserLoaded(aBrowser) {