Bug 1155730, implement History.scrollRestoration r=jst
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Sat, 26 Dec 2015 12:59:09 +0200
changeset 312788 84163eb4992b55414c20f471daacca9ac475d132
parent 312787 57d8ddc976a6aa4abfdb5d7aa650b7884abfdaba
child 312789 aa1170c267414e5d32d86d51098d56b61df1127e
push id5703
push userraliiev@mozilla.com
push dateMon, 07 Mar 2016 14:18:41 +0000
treeherdermozilla-beta@31e373ad5b5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjst
bugs1155730
milestone46.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
Bug 1155730, implement History.scrollRestoration r=jst
browser/components/sessionstore/SessionHistory.jsm
docshell/base/nsDocShell.cpp
docshell/base/nsIDocShell.idl
docshell/shistory/nsISHEntry.idl
docshell/shistory/nsSHEntry.cpp
docshell/shistory/nsSHEntry.h
docshell/test/navigation/file_scrollRestoration.html
docshell/test/navigation/mochitest.ini
docshell/test/navigation/test_sessionhistory.html
dom/base/nsHistory.cpp
dom/base/nsHistory.h
dom/webidl/History.webidl
layout/base/nsDocumentViewer.cpp
layout/base/nsILayoutHistoryState.h
layout/base/nsLayoutHistoryState.cpp
mobile/android/components/SessionStore.js
--- a/browser/components/sessionstore/SessionHistory.jsm
+++ b/browser/components/sessionstore/SessionHistory.jsm
@@ -149,20 +149,24 @@ var SessionHistoryInternal = {
       entry.isSrcdocEntry = shEntry.isSrcdocEntry;
 
     if (shEntry.baseURI)
       entry.baseURI = shEntry.baseURI.spec;
 
     if (shEntry.contentType)
       entry.contentType = shEntry.contentType;
 
-    let x = {}, y = {};
-    shEntry.getScrollPosition(x, y);
-    if (x.value != 0 || y.value != 0)
-      entry.scroll = x.value + "," + y.value;
+    if (shEntry.scrollRestorationIsManual) {
+      entry.scrollRestorationIsManual = true;
+    } else {
+      let x = {}, y = {};
+      shEntry.getScrollPosition(x, y);
+      if (x.value != 0 || y.value != 0)
+        entry.scroll = x.value + "," + y.value;
+    }
 
     // Collect owner data for the current history entry.
     try {
       let owner = this.serializeOwner(shEntry);
       if (owner) {
         entry.owner_b64 = owner;
       }
     } catch (ex) {
@@ -333,17 +337,19 @@ var SessionHistoryInternal = {
       shEntry.stateData =
         Cc["@mozilla.org/docshell/structured-clone-container;1"].
         createInstance(Ci.nsIStructuredCloneContainer);
 
       shEntry.stateData.initFromBase64(entry.structuredCloneState,
                                        entry.structuredCloneVersion);
     }
 
-    if (entry.scroll) {
+    if (entry.scrollRestorationIsManual) {
+      shEntry.scrollRestorationIsManual = true;
+    } else if (entry.scroll) {
       var scrollPos = (entry.scroll || "0,0").split(",");
       scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
       shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
     }
 
     let childDocIdents = {};
     if (entry.docIdentifier) {
       // If we have a serialized document identifier, try to find an SHEntry
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -10038,33 +10038,40 @@ nsDocShell::InternalLoad(nsIURI* aURI,
       // flag on firing onLocationChange(...).
       // Anyway, aCloneSHChildren param is simply reflecting
       // doShortCircuitedLoad in this scope.
       OnNewURI(aURI, nullptr, owner, mLoadType, true, true, true);
 
       nsCOMPtr<nsIInputStream> postData;
       nsCOMPtr<nsISupports> cacheKey;
 
+      bool scrollRestorationIsManual = false;
       if (mOSHE) {
         /* save current position of scroller(s) (bug 59774) */
         mOSHE->SetScrollPosition(cx, cy);
+        mOSHE->GetScrollRestorationIsManual(&scrollRestorationIsManual);
         // Get the postdata and page ident from the current page, if
         // the new load is being done via normal means.  Note that
         // "normal means" can be checked for just by checking for
         // LOAD_CMD_NORMAL, given the loadType and allowScroll check
         // above -- it filters out some LOAD_CMD_NORMAL cases that we
         // wouldn't want here.
         if (aLoadType & LOAD_CMD_NORMAL) {
           mOSHE->GetPostData(getter_AddRefs(postData));
           mOSHE->GetCacheKey(getter_AddRefs(cacheKey));
 
           // Link our new SHEntry to the old SHEntry's back/forward
           // cache data, since the two SHEntries correspond to the
           // same document.
           if (mLSHE) {
+            if (!aSHEntry) {
+              // If we're not doing a history load, scroll restoration
+              // should be inherited from the previous session history entry.
+              mLSHE->SetScrollRestorationIsManual(scrollRestorationIsManual);
+            }
             mLSHE->AdoptBFCacheEntry(mOSHE);
           }
         }
       }
 
       /* Assign mOSHE to mLSHE. This will either be a new entry created
        * by OnNewURI() for normal loads or aSHEntry for history loads.
        */
@@ -10127,20 +10134,22 @@ nsDocShell::InternalLoad(nsIURI* aURI,
       // ScrollToAnchor performs other important tasks, such as informing
       // the presShell that we have a new hash.  See bug 680257.
       rv = ScrollToAnchor(curHash, newHash, aLoadType);
       NS_ENSURE_SUCCESS(rv, rv);
 
       /* restore previous position of scroller(s), if we're moving
        * back in history (bug 59774)
        */
-      nscoord bx, by;
+      nscoord bx = 0;
+      nscoord by = 0;
       bool needsScrollPosUpdate = false;
       if (mOSHE && (aLoadType == LOAD_HISTORY ||
-                    aLoadType == LOAD_RELOAD_NORMAL)) {
+                    aLoadType == LOAD_RELOAD_NORMAL) &&
+          !scrollRestorationIsManual) {
         needsScrollPosUpdate = true;
         mOSHE->GetScrollPosition(&bx, &by);
       }
 
       // Dispatch the popstate and hashchange events, as appropriate.
       //
       // The event dispatch below can cause us to re-enter script and
       // destroy the docshell, nulling out mScriptGlobal. Hold a stack
@@ -11619,24 +11628,31 @@ nsDocShell::AddState(JS::Handle<JS::Valu
   nsCOMPtr<nsISHEntry> newSHEntry;
   if (!aReplace) {
     // Save the current scroll position (bug 590573).
     nscoord cx = 0, cy = 0;
     GetCurScrollPos(ScrollOrientation_X, &cx);
     GetCurScrollPos(ScrollOrientation_Y, &cy);
     mOSHE->SetScrollPosition(cx, cy);
 
+    bool scrollRestorationIsManual = false;
+    mOSHE->GetScrollRestorationIsManual(&scrollRestorationIsManual);
+
     // Since we're not changing which page we have loaded, pass
     // true for aCloneChildren.
     rv = AddToSessionHistory(newURI, nullptr, nullptr, true,
                              getter_AddRefs(newSHEntry));
     NS_ENSURE_SUCCESS(rv, rv);
 
     NS_ENSURE_TRUE(newSHEntry, NS_ERROR_FAILURE);
 
+    // Session history entries created by pushState inherit scroll restoration
+    // mode from the current entry.
+    newSHEntry->SetScrollRestorationIsManual(scrollRestorationIsManual);
+
     // Link the new SHEntry to the old SHEntry's BFCache entry, since the
     // two entries correspond to the same document.
     NS_ENSURE_SUCCESS(newSHEntry->AdoptBFCacheEntry(oldOSHE), NS_ERROR_FAILURE);
 
     // Set the new SHEntry's title (bug 655273).
     nsString title;
     mOSHE->GetTitle(getter_Copies(title));
     newSHEntry->SetTitle(title);
@@ -11735,16 +11751,37 @@ nsDocShell::AddState(JS::Handle<JS::Valu
   } else {
     FireDummyOnLocationChange();
   }
   document->SetStateObject(scContainer);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDocShell::GetCurrentScrollRestorationIsManual(bool* aIsManual)
+{
+  *aIsManual = false;
+  if (mOSHE) {
+    mOSHE->GetScrollRestorationIsManual(aIsManual);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetCurrentScrollRestorationIsManual(bool aIsManual)
+{
+  if (mOSHE) {
+    mOSHE->SetScrollRestorationIsManual(aIsManual);
+  }
+
+  return NS_OK;
+}
+
 bool
 nsDocShell::ShouldAddToSessionHistory(nsIURI* aURI)
 {
   // I believe none of the about: urls should go in the history. But then
   // that could just be me... If the intent is only deny about:blank then we
   // should just do a spec compare, rather than two gets of the scheme and
   // then the path.  -Gagan
   nsresult rv;
@@ -12135,20 +12172,31 @@ nsDocShell::GetShouldSaveLayoutState(boo
 }
 
 nsresult
 nsDocShell::PersistLayoutHistoryState()
 {
   nsresult rv = NS_OK;
 
   if (mOSHE) {
+    bool scrollRestorationIsManual = false;
+    mOSHE->GetScrollRestorationIsManual(&scrollRestorationIsManual);
+
     nsCOMPtr<nsIPresShell> shell = GetPresShell();
+    nsCOMPtr<nsILayoutHistoryState> layoutState;
     if (shell) {
-      nsCOMPtr<nsILayoutHistoryState> layoutState;
       rv = shell->CaptureHistoryState(getter_AddRefs(layoutState));
+    } else if (scrollRestorationIsManual) {
+      // Even if we don't have layout anymore, we may want to reset the current
+      // scroll state in layout history.
+      GetLayoutHistoryState(getter_AddRefs(layoutState));
+    }
+
+    if (scrollRestorationIsManual && layoutState) {
+      layoutState->ResetScrollState();
     }
   }
 
   return rv;
 }
 
 /* static */ nsresult
 nsDocShell::WalkHistoryEntries(nsISHEntry* aRootEntry,
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -38,17 +38,17 @@ interface nsIPrincipal;
 interface nsIWebBrowserPrint;
 interface nsIPrivacyTransitionObserver;
 interface nsIReflowObserver;
 interface nsIScrollObserver;
 interface nsITabParent;
 
 typedef unsigned long nsLoadFlags;
 
-[scriptable, builtinclass, uuid(bc3524bd-023c-4fc8-ace1-472bc999fb12)]
+[scriptable, builtinclass, uuid(811aa3e1-7c4d-45ae-89da-ea1b107c60ed)]
 interface nsIDocShell : nsIDocShellTreeItem
 {
   /**
    * Loads a given URI.  This will give priority to loading the requested URI
    * in the object implementing	this interface.  If it can't be loaded here
    * however, the URL dispatcher will go through its normal process of content
    * loading.
    *
@@ -1085,9 +1085,15 @@ interface nsIDocShell : nsIDocShellTreeI
   attribute DOMString paymentRequestId;
 
   /**
    * Allow usage of -moz-window-dragging:drag for content docshells.
    * True for top level chrome docshells. Throws if set to false with
    * top level chrome docshell.
    */
   attribute boolean windowDraggingAllowed;
+
+  /**
+   * Sets/gets the current scroll restoration mode.
+   * @see https://html.spec.whatwg.org/#dom-history-scroll-restoration
+  */
+  attribute boolean currentScrollRestorationIsManual;
 };
--- a/docshell/shistory/nsISHEntry.idl
+++ b/docshell/shistory/nsISHEntry.idl
@@ -25,17 +25,17 @@ interface nsIBFCacheEntry;
 #include "nsRect.h"
 class nsDocShellEditorData;
 class nsSHEntryShared;
 %}
 [ref] native nsIntRect(nsIntRect);
 [ptr] native nsDocShellEditorDataPtr(nsDocShellEditorData);
 [ptr] native nsSHEntryShared(nsSHEntryShared);
 
-[scriptable, uuid(3a5e5fa0-5364-4fbb-a87a-3f12a6b51903)]
+[scriptable, uuid(0dad26b8-a259-42c7-93f1-2fa7fc076e45)]
 interface nsISHEntry : nsISupports
 {
     /**
      * A readonly property that returns the URI
      * of the current entry. The object returned is
      * of type nsIURI
      */
     readonly attribute nsIURI URI;
@@ -296,16 +296,22 @@ interface nsISHEntry : nsISupports
     attribute AString srcdocData;
 
     /**
      * When isSrcdocEntry is true, this contains the baseURI of the srcdoc
      * document for use in situations where it cannot otherwise be determined,
      * for example with view-source.
      */
     attribute nsIURI baseURI;
+
+    /**
+     * Sets/gets the current scroll restoration state,
+     * if true == "manual", false == "auto".
+     */
+    attribute boolean scrollRestorationIsManual;
 };
 
 [scriptable, uuid(bb66ac35-253b-471f-a317-3ece940f04c5)]
 interface nsISHEntryInternal : nsISupports
 {
     [notxpcom] void RemoveFromBFCacheAsync();
     [notxpcom] void RemoveFromBFCacheSync();
 
--- a/docshell/shistory/nsSHEntry.cpp
+++ b/docshell/shistory/nsSHEntry.cpp
@@ -28,16 +28,17 @@ nsSHEntry::nsSHEntry()
   , mReferrerPolicy(mozilla::net::RP_Default)
   , mLoadType(0)
   , mID(gEntryID++)
   , mScrollPositionX(0)
   , mScrollPositionY(0)
   , mParent(nullptr)
   , mURIWasModified(false)
   , mIsSrcdocEntry(false)
+  , mScrollRestorationIsManual(false)
 {
 }
 
 nsSHEntry::nsSHEntry(const nsSHEntry& aOther)
   : mShared(aOther.mShared)
   , mURI(aOther.mURI)
   , mOriginalURI(aOther.mOriginalURI)
   , mLoadReplace(aOther.mLoadReplace)
@@ -48,16 +49,17 @@ nsSHEntry::nsSHEntry(const nsSHEntry& aO
   , mLoadType(0)         // XXX why not copy?
   , mID(aOther.mID)
   , mScrollPositionX(0)  // XXX why not copy?
   , mScrollPositionY(0)  // XXX why not copy?
   , mParent(aOther.mParent)
   , mURIWasModified(aOther.mURIWasModified)
   , mStateData(aOther.mStateData)
   , mIsSrcdocEntry(aOther.mIsSrcdocEntry)
+  , mScrollRestorationIsManual(false)
   , mSrcdocData(aOther.mSrcdocData)
   , mBaseURI(aOther.mBaseURI)
 {
 }
 
 static bool
 ClearParentPtr(nsISHEntry* aEntry, void* /* aData */)
 {
@@ -594,16 +596,30 @@ nsSHEntry::GetBaseURI(nsIURI** aBaseURI)
 NS_IMETHODIMP
 nsSHEntry::SetBaseURI(nsIURI* aBaseURI)
 {
   mBaseURI = aBaseURI;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsSHEntry::GetScrollRestorationIsManual(bool* aIsManual)
+{
+  *aIsManual = mScrollRestorationIsManual;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetScrollRestorationIsManual(bool aIsManual)
+{
+  mScrollRestorationIsManual = aIsManual;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsSHEntry::GetChildCount(int32_t* aCount)
 {
   *aCount = mChildren.Count();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::AddChild(nsISHEntry* aChild, int32_t aOffset)
--- a/docshell/shistory/nsSHEntry.h
+++ b/docshell/shistory/nsSHEntry.h
@@ -59,13 +59,14 @@ private:
   uint32_t mID;
   int32_t mScrollPositionX;
   int32_t mScrollPositionY;
   nsISHEntry* mParent;
   nsCOMArray<nsISHEntry> mChildren;
   bool mURIWasModified;
   nsCOMPtr<nsIStructuredCloneContainer> mStateData;
   bool mIsSrcdocEntry;
+  bool mScrollRestorationIsManual;
   nsString mSrcdocData;
   nsCOMPtr<nsIURI> mBaseURI;
 };
 
 #endif /* nsSHEntry_h */
new file mode 100644
--- /dev/null
+++ b/docshell/test/navigation/file_scrollRestoration.html
@@ -0,0 +1,128 @@
+<html>
+  <head>
+    <script>
+      var oldHistoryObject = null;
+
+      function test(event) {
+        if (!opener.scrollRestorationTest) {
+          opener.scrollRestorationTest = 0;
+        }
+        ++opener.scrollRestorationTest;
+
+        switch (opener.scrollRestorationTest) {
+          case 1: {
+            opener.is(event.persisted, false, "Shouldn't have persisted session history entry.");
+            opener.ok(history.scrollRestoration, "History object has scrollRestoration property.");
+            opener.ok(history.scrollRestoration, "auto", "history.scrollRestoration's default value should be 'auto'.");
+            history.scrollRestoration = "foobar";
+            opener.ok(history.scrollRestoration, "auto", "Invalid enum value should not change the value of an attribute.");
+            history.scrollRestoration = "manual";
+            opener.ok(history.scrollRestoration, "manual", "Valid enum value should change the value of an attribute.");
+            history.scrollRestoration = "auto";
+            opener.ok(history.scrollRestoration, "auto", "Valid enum value should change the value of an attribute.");
+            document.getElementById("bottom").scrollIntoView();
+            window.location.reload(false);
+            break;
+          }
+          case 2: {
+            opener.is(event.persisted, false, "Shouldn't have persisted session history entry.");
+            opener.isnot(window.scrollY, 0, "Should have restored scrolling.");
+            opener.is(history.scrollRestoration, "auto", "Should have the same scrollRestoration as before reload.");
+            history.scrollRestoration = "manual";
+            window.onunload = function() {} // Disable bfcache.
+            window.location.reload(false);
+            break;
+          }
+          case 3: {
+            opener.is(event.persisted, false, "Shouldn't have persisted session history entry.");
+            opener.is(window.scrollY, 0, "Should not have restored scrolling.");
+            opener.is(history.scrollRestoration, "manual", "Should have the same scrollRestoration as before reload.");
+            document.getElementById("bottom").scrollIntoView();
+            window.onunload = null; // Should get bfcache behavior.
+            opener.setTimeout("testWindow.history.back();", 250);
+            window.location.href = 'data:text/html,';
+            break;
+          }
+          case 4: {
+            opener.is(event.persisted, true, "Should have persisted session history entry.");
+            opener.isnot(window.scrollY, 0, "Should have kept the old scroll position.");
+            opener.is(history.scrollRestoration, "manual", "Should have the same scrollRestoration as before reload.");
+            window.scrollTo(0, 0);
+            window.location.hash = "hash";
+            requestAnimationFrame(test);
+            break;
+          }
+          case 5: {
+            opener.isnot(window.scrollY, 0, "Should have scrolled to #hash.");
+            opener.is(history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation.");
+            window.onunload = function() {} // Disable bfcache.
+            opener.setTimeout("is(testWindow.history.scrollRestoration, 'auto'); testWindow.history.back();", 250);
+            window.location.href = 'data:text/html,';
+            break;
+          }
+          case 6: {
+            opener.is(event.persisted, false, "Shouldn't have persisted session history entry.");
+            opener.is(window.scrollY, 0, "Shouldn't have kept the old scroll position.");
+            opener.is(history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation.");
+            history.scrollRestoration = "auto";
+            document.getElementById("bottom").scrollIntoView();
+            history.pushState({ state: "state1" }, "state1");
+            history.pushState({ state: "state2" }, "state2");
+            window.scrollTo(0, 0);
+            history.back();
+            opener.isnot(window.scrollY, 0, "Should have scrolled back to the state1's position");
+            opener.is(history.state.state, "state1", "Unexpected state.");
+
+            history.scrollRestoration = "manual";
+            document.getElementById("bottom").scrollIntoView();
+            history.pushState({ state: "state3" }, "state3");
+            history.pushState({ state: "state4" }, "state4");
+            window.scrollTo(0, 0);
+            history.back();
+            opener.is(window.scrollY, 0, "Shouldn't have scrolled back to the state3's position");
+            opener.is(history.state.state, "state3", "Unexpected state.");
+
+            var ifr = document.createElement("iframe");
+            ifr.src = "data:text/html,";
+            document.body.appendChild(ifr);
+            ifr.onload = test;
+            break;
+          }
+          case 7: {
+            oldHistoryObject = event.target.contentWindow.history;
+            event.target.src = "about:blank";
+            break;
+          }
+          case 8: {
+            try {
+              var sr = oldHistoryObject.scrollRestoration;
+              opener.ok(false, "Should have thrown an exception.");
+            } catch(ex) {
+              opener.isnot(ex, null, "Did get an exception");
+            }
+            try {
+              oldHistoryObject.scrollRestoration = "auto";
+              opener.ok(false, "Should have thrown an exception.");
+            } catch(ex) {
+              opener.isnot(ex, null, "Did get an exception");
+            }
+            opener.nextTest();
+            window.close();
+            break;
+          }
+        }
+      }
+
+      window.addEventListener("pageshow",
+          function(e) {
+            setTimeout(test, 0, e);
+          });
+    </script>
+  </head>
+  <body>
+  <div style="border: 1px solid black; height: 5000px;">
+  &nbsp;</div>
+  <div id="bottom">Hello world</div>
+  <a href="#hash" name="hash">hash</a>
+  </body>
+</html>
--- a/docshell/test/navigation/mochitest.ini
+++ b/docshell/test/navigation/mochitest.ini
@@ -5,16 +5,17 @@ support-files =
   file_bug462076_1.html
   file_bug462076_2.html
   file_bug462076_3.html
   file_bug508537_1.html
   file_bug534178.html
   file_document_write_1.html
   file_fragment_handling_during_load.html
   file_nested_frames.html
+  file_scrollRestoration.html
   file_shiftReload_and_pushState.html
   file_static_and_dynamic_1.html
   frame0.html
   frame1.html
   frame2.html
   frame3.html
   goback.html
   iframe.html
--- a/docshell/test/navigation/test_sessionhistory.html
+++ b/docshell/test/navigation/test_sessionhistory.html
@@ -24,17 +24,18 @@ var testFiles =
     "file_bug462076_2.html",         // Dynamic frames when handling onload
     "file_bug462076_3.html",         // Dynamic frames after onload
     "file_bug508537_1.html",         // Dynamic frames and forward-back
     "file_document_write_1.html",    // Session history + document.write
     //"file_static_and_dynamic_1.html",// Static and dynamic frames and forward-back
     "file_bug534178.html",           // Session history transaction clean-up.
     "file_fragment_handling_during_load.html",
     "file_nested_frames.html",
-    "file_shiftReload_and_pushState.html"
+    "file_shiftReload_and_pushState.html",
+    "file_scrollRestoration.html"
   ];
 var testCount = 0; // Used by the test files.
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.requestFlakyTimeout("untriaged");
 
 var testWindow;
 function nextTest_() {
--- a/dom/base/nsHistory.cpp
+++ b/dom/base/nsHistory.cpp
@@ -2,17 +2,16 @@
 /* vim: set ts=8 sts=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 "nsHistory.h"
 
 #include "jsapi.h"
-#include "mozilla/dom/HistoryBinding.h"
 #include "nsCOMPtr.h"
 #include "nsPIDOMWindow.h"
 #include "nsIDocument.h"
 #include "nsIPresShell.h"
 #include "nsPresContext.h"
 #include "nsIDocShell.h"
 #include "nsIWebNavigation.h"
 #include "nsIURI.h"
@@ -91,16 +90,48 @@ nsHistory::GetLength(ErrorResult& aRv) c
     aRv.Throw(rv);
 
     return 0;
   }
 
   return len >= 0 ? len : 0;
 }
 
+ScrollRestoration
+nsHistory::GetScrollRestoration(mozilla::ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindow> win(do_QueryReferent(mInnerWindow));
+  if (!win || !win->HasActiveDocument() || !win->GetDocShell()) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return mozilla::dom::ScrollRestoration::Auto;
+  }
+
+  bool currentScrollRestorationIsManual = false;
+  win->GetDocShell()->
+    GetCurrentScrollRestorationIsManual(&currentScrollRestorationIsManual);
+  return currentScrollRestorationIsManual ?
+    mozilla::dom::ScrollRestoration::Manual :
+    mozilla::dom::ScrollRestoration::Auto;
+}
+
+void
+nsHistory::SetScrollRestoration(mozilla::dom::ScrollRestoration aMode,
+                                mozilla::ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindow> win(do_QueryReferent(mInnerWindow));
+  if (!win || !win->HasActiveDocument() || !win->GetDocShell()) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  win->GetDocShell()->
+    SetCurrentScrollRestorationIsManual(
+      aMode == mozilla::dom::ScrollRestoration::Manual);
+}
+
 void
 nsHistory::GetState(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
                     ErrorResult& aRv) const
 {
   nsCOMPtr<nsPIDOMWindow> win(do_QueryReferent(mInnerWindow));
   if (!win) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return;
--- a/dom/base/nsHistory.h
+++ b/dom/base/nsHistory.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #ifndef nsHistory_h___
 #define nsHistory_h___
 
 #include "mozilla/Attributes.h"
 #include "mozilla/ErrorResult.h"
+#include "mozilla/dom/HistoryBinding.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIDOMHistory.h"
 #include "nsPIDOMWindow.h" // for GetParentObject
 #include "nsStringFwd.h"
 #include "nsWrapperCache.h"
 
 class nsIDocShell;
@@ -31,16 +32,19 @@ public:
 
 public:
   explicit nsHistory(nsPIDOMWindow* aInnerWindow);
 
   nsPIDOMWindow* GetParentObject() const;
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   uint32_t GetLength(mozilla::ErrorResult& aRv) const;
+  mozilla::dom::ScrollRestoration GetScrollRestoration(mozilla::ErrorResult& aRv);
+  void SetScrollRestoration(mozilla::dom::ScrollRestoration aMode,
+                            mozilla::ErrorResult& aRv);
   void GetState(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
                 mozilla::ErrorResult& aRv) const;
   void Go(int32_t aDelta, mozilla::ErrorResult& aRv);
   void Back(mozilla::ErrorResult& aRv);
   void Forward(mozilla::ErrorResult& aRv);
   void PushState(JSContext* aCx, JS::Handle<JS::Value> aData,
                  const nsAString& aTitle, const nsAString& aUrl,
                  mozilla::ErrorResult& aRv);
--- a/dom/webidl/History.webidl
+++ b/dom/webidl/History.webidl
@@ -6,20 +6,24 @@
  * The origin of this IDL file is
  * http://www.whatwg.org/specs/web-apps/current-work/#the-history-interface
  *
  * © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and
  * Opera Software ASA. You are granted a license to use, reproduce
  * and create derivative works of this document.
  */
 
+enum ScrollRestoration { "auto", "manual" };
+
 interface History {
   [Throws]
   readonly attribute unsigned long length;
   [Throws]
+  attribute ScrollRestoration scrollRestoration;
+  [Throws]
   readonly attribute any state;
   [Throws, UnsafeInPrerendering]
   void go(optional long delta = 0);
   [Throws, UnsafeInPrerendering]
   void back();
   [Throws, UnsafeInPrerendering]
   void forward();
   [Throws]
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -2079,16 +2079,21 @@ nsDocumentViewer::Hide(void)
     // and we don't want the presshell n' all that to be thrown away
     // just because the window is hidden.
 
     return NS_OK;
   }
 
   nsCOMPtr<nsIDocShell> docShell(mContainer);
   if (docShell) {
+#ifdef DEBUG
+    nsCOMPtr<nsIContentViewer> currentViewer;
+    docShell->GetContentViewer(getter_AddRefs(currentViewer));
+    MOZ_ASSERT(currentViewer == this);
+#endif
     nsCOMPtr<nsILayoutHistoryState> layoutState;
     mPresShell->CaptureHistoryState(getter_AddRefs(layoutState));
   }
 
   DestroyPresShell();
 
   DestroyPresContext();
 
--- a/layout/base/nsILayoutHistoryState.h
+++ b/layout/base/nsILayoutHistoryState.h
@@ -13,18 +13,18 @@
 
 #include "nsISupports.h"
 #include "nsStringFwd.h"
 
 class nsPresState;
 template<typename> struct already_AddRefed;
 
 #define NS_ILAYOUTHISTORYSTATE_IID \
-{ 0x5208993e, 0xd812, 0x431e, \
-  { 0x95, 0x9c, 0xc3, 0x84, 0x5b, 0x6e, 0x5a, 0xce } }
+{ 0xaef27cb3, 0x4df9, 0x4eeb, \
+  { 0xb0, 0xb0, 0xac, 0x56, 0xcf, 0x86, 0x1d, 0x04 } }
 
 class nsILayoutHistoryState : public nsISupports {
  public: 
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_ILAYOUTHISTORYSTATE_IID)
 
   /**
    * Set |aState| as the state object for |aKey|.
    * This _transfers_ownership_ of |aState| to the LayoutHistoryState.
@@ -48,16 +48,21 @@ class nsILayoutHistoryState : public nsI
    */
   virtual bool HasStates() const = 0;
 
   /**
    * Sets whether this history can contain only scroll position history
    * or all possible history
    */
   virtual void SetScrollPositionOnly(const bool aFlag) = 0;
+
+  /**
+   * Resets nsPresState::GetScrollState of all nsPresState objects to 0,0.
+   */
+  virtual void ResetScrollState() = 0;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsILayoutHistoryState,
                               NS_ILAYOUTHISTORYSTATE_IID)
 
 already_AddRefed<nsILayoutHistoryState>
 NS_NewLayoutHistoryState();
 
--- a/layout/base/nsLayoutHistoryState.cpp
+++ b/layout/base/nsLayoutHistoryState.cpp
@@ -31,17 +31,18 @@ public:
   virtual nsPresState*
   GetState(const nsCString& aKey) override;
   virtual void
   RemoveState(const nsCString& aKey) override;
   virtual bool
   HasStates() const override;
   virtual void
   SetScrollPositionOnly(const bool aFlag) override;
-
+  virtual void
+  ResetScrollState() override;
 
 private:
   ~nsLayoutHistoryState() {}
   bool mScrollPositionOnly;
 
   nsClassHashtable<nsCStringHashKey,nsPresState> mStates;
 };
 
@@ -89,8 +90,19 @@ nsLayoutHistoryState::HasStates() const
   return mStates.Count() != 0;
 }
 
 void
 nsLayoutHistoryState::SetScrollPositionOnly(const bool aFlag)
 {
   mScrollPositionOnly = aFlag;
 }
+
+void
+nsLayoutHistoryState::ResetScrollState()
+{
+  for (auto iter = mStates.Iter(); !iter.Done(); iter.Next()) {
+    nsPresState* state = iter.UserData();
+    if (state) {
+      state->SetScrollState(nsPoint(0, 0));
+    }
+  }
+}
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -751,20 +751,24 @@ SessionStore.prototype = {
     if (aEntry.referrerURI) {
       entry.referrer = aEntry.referrerURI.spec;
     }
 
     if (aEntry.contentType) {
       entry.contentType = aEntry.contentType;
     }
 
-    let x = {}, y = {};
-    aEntry.getScrollPosition(x, y);
-    if (x.value != 0 || y.value != 0) {
-      entry.scroll = x.value + "," + y.value;
+    if (aEntry.scrollRestorationIsManual) {
+      entry.scrollRestorationIsManual = true;
+    } else {
+      let x = {}, y = {};
+      aEntry.getScrollPosition(x, y);
+      if (x.value != 0 || y.value != 0) {
+        entry.scroll = x.value + "," + y.value;
+      }
     }
 
     if (aEntry.owner) {
       try {
         let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream);
         let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
         pipe.init(false, false, 0, 0xffffffff, null);
         binaryStream.setOutputStream(pipe.outputStream);
@@ -857,17 +861,19 @@ SessionStore.prototype = {
     if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
       shEntry.stateData =
         Cc["@mozilla.org/docshell/structured-clone-container;1"].
         createInstance(Ci.nsIStructuredCloneContainer);
 
       shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion);
     }
 
-    if (aEntry.scroll) {
+    if (aEntry.scrollRestorationIsManual) {
+      shEntry.scrollRestorationIsManual = true;
+    } else if (aEntry.scroll) {
       let scrollPos = aEntry.scroll.split(",");
       scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
       shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
     }
 
     let childDocIdents = {};
     if (aEntry.docIdentifier) {
       // If we have a serialized document identifier, try to find an SHEntry