Bug 1310768 - Use CreataAboutBlankContentViewer to stop inactive tabs. Update History API. a=ehsan
authorSamael Wang <freesamael@gmail.com>
Tue, 29 Nov 2016 17:31:17 +0800
changeset 324657 fcb1d52e5a8213fcfe7e9e5ebfef908122e835c4
parent 324656 e0418e43a56a778989013b446643bd45e2578708
child 324658 4a3b9385a2e231be238863c780313351d2d7b263
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersehsan
bugs1310768
milestone53.0a1
Bug 1310768 - Use CreataAboutBlankContentViewer to stop inactive tabs. Update History API. a=ehsan MozReview-Commit-ID: LPcaQkQ900G
docshell/base/nsDocShell.cpp
docshell/shistory/nsISHistory.idl
docshell/shistory/nsISHistoryListener.idl
docshell/shistory/nsSHistory.cpp
dom/base/nsHistory.cpp
dom/base/test/chrome/window_groupedSHistory.xul
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -11592,16 +11592,20 @@ nsDocShell::OnNewURI(nsIURI* aURI, nsICh
   //     and |updateGHistory| are not correct at this point.
 
   MOZ_LOG(gDocShellLog, LogLevel::Debug,
          ("  shAvailable=%i updateSHistory=%i updateGHistory=%i"
           " equalURI=%i\n",
           shAvailable, updateSHistory, updateGHistory, equalUri));
 
   if (shAvailable && mCurrentURI && !mOSHE && aLoadType != LOAD_ERROR_PAGE) {
+    // XXX mCurrentURI can be changed from any caller regardless what actual
+    // loaded document is, so testing mCurrentURI isn't really a reliable way.
+    // Session restore is one example which changes current URI in order to
+    // show address before loading. See bug 1301399.
     NS_ASSERTION(NS_IsAboutBlank(mCurrentURI),
                  "no SHEntry for a non-transient viewer?");
   }
 #endif
 
   /* If the url to be loaded is the same as the one already there,
    * and the original loadType is LOAD_NORMAL, LOAD_LINK, or
    * LOAD_STOP_CONTENT, set loadType to LOAD_NORMAL_REPLACE so that
--- a/docshell/shistory/nsISHistory.idl
+++ b/docshell/shistory/nsISHistory.idl
@@ -68,17 +68,22 @@ interface nsISHistory: nsISupports
 
   /**
    * A readonly property of the interface that returns
    * the index of the current document in session history.
    */
    readonly attribute long index;
 
   /**
-   * A readonly property of the interface that returns 
+   * A readonly property which equals index + globalIndexOffset.
+   */
+   readonly attribute long globalIndex;
+
+  /**
+   * A readonly property of the interface that returns
    * the index of the last document that started to load and
    * didn't finished yet. When document finishes the loading
    * value -1 is returned.
    */
    readonly attribute long requestedIndex;
 
   /**
    * A read/write property of the interface, used to Get/Set
@@ -99,16 +104,23 @@ interface nsISHistory: nsISupports
    *
    * @return                  <code>NS_OK</code> history entry for 
    *                          the index is obtained successfully.
    *                          <code>NS_ERROR_FAILURE</code> Error in obtaining
    *                          history entry for the given index.
    */
    nsISHEntry getEntryAtIndex(in long index, in boolean modifyIndex);
 
+  /**
+   * Load the entry at given index to root docshell directly in order to
+   * restore to an entry in grouped session history navigation or session
+   * restore case. This function will not notify nsISHistoryListener, as it's
+   * not considered a normal history navigation.
+   */
+   void restoreToEntryAtIndex(in long index);
 
   /**
    * Called to purge older documents from history.
    * Documents can be removed from session history for various 
    * reasons. For example to  control memory usage of the browser, to 
    * prevent users from loading documents from history, to erase evidence of
    * prior page loads etc...
    *
--- a/docshell/shistory/nsISHistoryListener.idl
+++ b/docshell/shistory/nsISHistoryListener.idl
@@ -34,25 +34,31 @@ interface nsISHistoryListener : nsISuppo
    */
    void OnHistoryNewEntry(in nsIURI aNewURI, in long aOldIndex);
 
   /**
    * Called when navigating to a previous session history entry, for example
    * due to a nsIWebNavigation::goBack() call.
    *
    * @param aBackURI    The URI of the session history entry being navigated to.
+   *                    It could be null in case of a grouped session history
+   *                    navigation since we have no URI information of entries
+   *                    existing in other partial histories.
    * @return            Whether the operation can proceed.
    */
    boolean OnHistoryGoBack(in nsIURI aBackURI);
 
   /**
    * Called when navigating to a next session history entry, for example
    * due to a nsIWebNavigation::goForward() call.
    *
    * @param aForwardURI   The URI of the session history entry being navigated to.
+   *                      It could be null in case of a grouped session history
+   *                      navigation since we have no URI information of entries
+   *                      existing in other partial histories.
    * @return              Whether the operation can proceed.
    */
    boolean OnHistoryGoForward(in nsIURI aForwardURI);
 
   /** 
    * Called when the current document is reloaded, for example due to a
    * nsIWebNavigation::reload() call.
    *
@@ -67,16 +73,19 @@ interface nsISHistoryListener : nsISuppo
    boolean OnHistoryReload(in nsIURI aReloadURI, in unsigned long aReloadFlags);
 
   /**
    * Called when navigating to a session history entry by index, for example,
    * when nsIWebNavigation::gotoIndex() is called.
    *
    * @param aIndex        The index in session history of the entry to be loaded.
    * @param aGotoURI      The URI of the session history entry to be loaded.
+   *                      It could be null in case of a grouped session history
+   *                      navigation since we have no URI information of entries
+   *                      existing in other partial histories.
    * @return              Whether the operation can proceed.
    */
    boolean OnHistoryGotoIndex(in long aIndex, in nsIURI aGotoURI);
 
   /**
    * Called when entries are removed from session history. Entries can be
    * removed from session history for various reasons, for example to control
    * the memory usage of the browser, to prevent users from loading documents
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -25,16 +25,17 @@
 #include "nsTArray.h"
 #include "nsCOMArray.h"
 #include "nsDocShell.h"
 #include "mozilla/Attributes.h"
 #include "nsISHEntry.h"
 #include "nsISHTransaction.h"
 #include "nsISHistoryListener.h"
 #include "nsComponentManagerUtils.h"
+#include "nsNetUtil.h"
 
 // For calculating max history entries and max cachable contentviewers
 #include "prsystem.h"
 #include "mozilla/MathAlgorithms.h"
 
 using namespace mozilla;
 
 #define PREF_SHISTORY_SIZE "browser.sessionhistory.max_entries"
@@ -480,58 +481,65 @@ nsSHistory::GetGlobalIndexOffset(int32_t
   NS_ENSURE_ARG_POINTER(aResult);
   *aResult = mGlobalIndexOffset;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::OnPartialSessionHistoryActive(int32_t aGlobalLength, int32_t aTargetIndex)
 {
-  NS_ENSURE_TRUE(mIsPartial, NS_ERROR_UNEXPECTED);
+  NS_ENSURE_TRUE(mRootDocShell && mIsPartial, NS_ERROR_UNEXPECTED);
 
   int32_t extraLength = aGlobalLength - mLength - mGlobalIndexOffset;
   NS_ENSURE_TRUE(extraLength >= 0, NS_ERROR_UNEXPECTED);
 
   if (extraLength != mEntriesInFollowingPartialHistories) {
     mEntriesInFollowingPartialHistories = extraLength;
   }
 
-  if (mIndex == aTargetIndex) {
-    // TODO When we finish OnPartialSessionHistoryDeactive, we'll need to active
-    // the suspended document here.
-
-    // Fire location change to update canGoBack / canGoForward.
-    NS_DispatchToCurrentThread(NewRunnableMethod(static_cast<nsDocShell*>(mRootDocShell),
-                                                 &nsDocShell::FireDummyOnLocationChange));
-    return NS_OK;
-  }
-
-  return LoadEntry(aTargetIndex, nsIDocShellLoadInfo::loadHistory,
-                   HIST_CMD_GOTOINDEX);
+  return RestoreToEntryAtIndex(aTargetIndex);
 }
 
 NS_IMETHODIMP
 nsSHistory::OnPartialSessionHistoryDeactive()
 {
-  NS_ENSURE_TRUE(mIsPartial, NS_ERROR_UNEXPECTED);
+  NS_ENSURE_TRUE(mRootDocShell && mIsPartial, NS_ERROR_UNEXPECTED);
 
-  // TODO We need to suspend current document first. Much like what happens when
-  // loading a new page. Move the ownership of the document to nsISHEntry or so.
+  // Ensure the deactive docshell loads about:blank.
+  nsCOMPtr<nsIWebNavigation> webNav = do_QueryInterface(mRootDocShell);
+  nsCOMPtr<nsIURI> currentURI;
+  webNav->GetCurrentURI(getter_AddRefs(currentURI));
+  if (NS_IsAboutBlank(currentURI)) {
+    return NS_OK;
+  }
+
+  if (NS_FAILED(mRootDocShell->CreateAboutBlankContentViewer(nullptr))) {
+    return NS_ERROR_FAILURE;
+  }
+
   return NS_OK;
 }
 
 /* Get index of the history list */
 NS_IMETHODIMP
 nsSHistory::GetIndex(int32_t* aResult)
 {
   NS_PRECONDITION(aResult, "null out param?");
   *aResult = mIndex;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::GetGlobalIndex(int32_t* aResult)
+{
+  NS_PRECONDITION(aResult, "null out param?");
+  *aResult = mIndex + mGlobalIndexOffset;
+  return NS_OK;
+}
+
 /* Get the requestedIndex */
 NS_IMETHODIMP
 nsSHistory::GetRequestedIndex(int32_t* aResult)
 {
   NS_PRECONDITION(aResult, "null out param?");
   *aResult = mRequestedIndex;
   return NS_OK;
 }
@@ -1005,16 +1013,32 @@ nsSHistory::ReloadCurrentEntry()
                               (mIndex, currentURI, &canNavigate));
   if (!canNavigate) {
     return NS_OK;
   }
 
   return LoadEntry(mIndex, nsIDocShellLoadInfo::loadHistory, HIST_CMD_RELOAD);
 }
 
+NS_IMETHODIMP
+nsSHistory::RestoreToEntryAtIndex(int32_t aIndex)
+{
+  mRequestedIndex = aIndex;
+
+  nsCOMPtr<nsISHEntry> nextEntry;
+  GetEntryAtIndex(mRequestedIndex, false, getter_AddRefs(nextEntry));
+  if (!nextEntry) {
+    mRequestedIndex = -1;
+    return NS_ERROR_FAILURE;
+  }
+
+  // XXX We may want to ensure docshell is currently holding about:blank
+  return InitiateLoad(nextEntry, mRootDocShell, nsIDocShellLoadInfo::loadHistory);
+}
+
 void
 nsSHistory::EvictOutOfRangeWindowContentViewers(int32_t aIndex)
 {
   // XXX rename method to EvictContentViewersExceptAroundIndex, or something.
 
   // We need to release all content viewers that are no longer in the range
   //
   //  aIndex - gHistoryMaxViewers to aIndex + gHistoryMaxViewers
@@ -1633,62 +1657,64 @@ nsSHistory::LoadNextPossibleEntry(int32_
 
 NS_IMETHODIMP
 nsSHistory::LoadEntry(int32_t aIndex, long aLoadType, uint32_t aHistCmd)
 {
   if (!mRootDocShell) {
     return NS_ERROR_FAILURE;
   }
 
+  nsCOMPtr<nsIURI> nextURI;
+  nsCOMPtr<nsISHEntry> prevEntry;
+  nsCOMPtr<nsISHEntry> nextEntry;
+  bool isCrossBrowserNavigation = false;
   if (aIndex < 0 || aIndex >= mLength) {
     if (aIndex + mGlobalIndexOffset < 0) {
       // The global index is negative.
       return NS_ERROR_FAILURE;
     }
 
     if (aIndex - mLength >= mEntriesInFollowingPartialHistories) {
       // The global index exceeds max possible value.
       return NS_ERROR_FAILURE;
     }
 
-    // The global index is valid. trigger cross browser navigation.
-    nsCOMPtr<nsIPartialSHistoryListener> listener =
-      do_QueryReferent(mPartialHistoryListener);
-    if (!listener) {
+    // The global index is valid. Mark that we're going to navigate to another
+    // partial history, but wait until we've notified all listeners before
+    // actually do so.
+    isCrossBrowserNavigation = true;
+  } else {
+    // This is a normal local history navigation.
+    // Keep note of requested history index in mRequestedIndex.
+    mRequestedIndex = aIndex;
+
+    GetEntryAtIndex(mIndex, false, getter_AddRefs(prevEntry));
+    GetEntryAtIndex(mRequestedIndex, false, getter_AddRefs(nextEntry));
+    if (!nextEntry || !prevEntry) {
+      mRequestedIndex = -1;
       return NS_ERROR_FAILURE;
     }
-    return listener->OnRequestCrossBrowserNavigation(aIndex + mGlobalIndexOffset);
+
+    // Remember that this entry is getting loaded at this point in the sequence
+    nsCOMPtr<nsISHEntryInternal> entryInternal = do_QueryInterface(nextEntry);
+
+    if (entryInternal) {
+      entryInternal->SetLastTouched(++gTouchCounter);
+    }
+
+    // Get the uri for the entry we are about to visit
+    nextEntry->GetURI(getter_AddRefs(nextURI));
   }
 
-  // Keep note of requested history index in mRequestedIndex.
-  mRequestedIndex = aIndex;
-
-  nsCOMPtr<nsISHEntry> prevEntry;
-  GetEntryAtIndex(mIndex, false, getter_AddRefs(prevEntry));
-
-  nsCOMPtr<nsISHEntry> nextEntry;
-  GetEntryAtIndex(mRequestedIndex, false, getter_AddRefs(nextEntry));
-  if (!nextEntry || !prevEntry) {
-    mRequestedIndex = -1;
-    return NS_ERROR_FAILURE;
-  }
+  MOZ_ASSERT(isCrossBrowserNavigation || (prevEntry && nextEntry && nextURI),
+    "prevEntry, nextEntry and nextURI can be null only if isCrossBrowserNavigation is set");
 
-  // Remember that this entry is getting loaded at this point in the sequence
-  nsCOMPtr<nsISHEntryInternal> entryInternal = do_QueryInterface(nextEntry);
-
-  if (entryInternal) {
-    entryInternal->SetLastTouched(++gTouchCounter);
-  }
-
-  // Send appropriate listener notifications
+  // Send appropriate listener notifications. Note nextURI could be null in case
+  // of grouped session history navigation.
   bool canNavigate = true;
-  // Get the uri for the entry we are about to visit
-  nsCOMPtr<nsIURI> nextURI;
-  nextEntry->GetURI(getter_AddRefs(nextURI));
-
   if (aHistCmd == HIST_CMD_BACK) {
     // We are going back one entry. Send GoBack notifications
     NOTIFY_LISTENERS_CANCELABLE(OnHistoryGoBack, canNavigate,
                                 (nextURI, &canNavigate));
   } else if (aHistCmd == HIST_CMD_FORWARD) {
     // We are going forward. Send GoForward notification
     NOTIFY_LISTENERS_CANCELABLE(OnHistoryGoForward, canNavigate,
                                 (nextURI, &canNavigate));
@@ -1700,16 +1726,33 @@ nsSHistory::LoadEntry(int32_t aIndex, lo
 
   if (!canNavigate) {
     // If the listener asked us not to proceed with
     // the operation, simply return.
     mRequestedIndex = -1;
     return NS_OK;  // XXX Maybe I can return some other error code?
   }
 
+  if (isCrossBrowserNavigation) {
+    nsCOMPtr<nsIPartialSHistoryListener> listener =
+      do_QueryReferent(mPartialHistoryListener);
+    if (!listener) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // CreateAboutBlankContentViewer would check for permit unload, fire proper
+    // pagehide / unload events and transfer content viewer ownership to SHEntry.
+    if (NS_FAILED(mRootDocShell->CreateAboutBlankContentViewer(nullptr))) {
+      return NS_ERROR_FAILURE;
+    }
+
+    return listener->OnRequestCrossBrowserNavigation(aIndex +
+                                                     mGlobalIndexOffset);
+  }
+
   if (mRequestedIndex == mIndex) {
     // Possibly a reload case
     return InitiateLoad(nextEntry, mRootDocShell, aLoadType);
   }
 
   // Going back or forward.
   bool differenceFound = false;
   nsresult rv = LoadDifferingEntries(prevEntry, nextEntry, mRootDocShell,
@@ -1868,17 +1911,17 @@ nsSHistory::GetSHistoryEnumerator(nsISim
   RefPtr<nsSHEnumerator> iterator = new nsSHEnumerator(this);
   iterator.forget(aEnumerator);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::OnAttachGroupedSessionHistory(int32_t aOffset)
 {
-  NS_ENSURE_TRUE(!mIsPartial, NS_ERROR_UNEXPECTED);
+  NS_ENSURE_TRUE(!mIsPartial && mRootDocShell, NS_ERROR_UNEXPECTED);
   NS_ENSURE_TRUE(aOffset >= 0, NS_ERROR_ILLEGAL_VALUE);
 
   mIsPartial = true;
   mGlobalIndexOffset = aOffset;
 
   // The last attached history is always at the end of the group.
   mEntriesInFollowingPartialHistories = 0;
 
--- a/dom/base/nsHistory.cpp
+++ b/dom/base/nsHistory.cpp
@@ -74,17 +74,17 @@ nsHistory::GetLength(ErrorResult& aRv) c
   nsCOMPtr<nsISHistory> sHistory = GetSessionHistory();
   if (!sHistory) {
     aRv.Throw(NS_ERROR_FAILURE);
 
     return 0;
   }
 
   int32_t len;
-  nsresult rv = sHistory->GetCount(&len);
+  nsresult rv = sHistory->GetGlobalCount(&len);
 
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
 
     return 0;
   }
 
   return len >= 0 ? len : 0;
@@ -206,18 +206,18 @@ nsHistory::Go(int32_t aDelta, ErrorResul
   if (!webnav) {
     aRv.Throw(NS_ERROR_FAILURE);
 
     return;
   }
 
   int32_t curIndex = -1;
   int32_t len = 0;
-  session_history->GetIndex(&curIndex);
-  session_history->GetCount(&len);
+  session_history->GetGlobalIndex(&curIndex);
+  session_history->GetGlobalCount(&len);
 
   int32_t index = curIndex + aDelta;
   if (index > -1 && index < len)
     webnav->GotoIndex(index);
 
   // Ignore the return value from GotoIndex(), since returning errors
   // from GotoIndex() can lead to exceptions and a possible leak
   // of history length
--- a/dom/base/test/chrome/window_groupedSHistory.xul
+++ b/dom/base/test/chrome/window_groupedSHistory.xul
@@ -1,16 +1,22 @@
 <?xml version="1.0"?>
 <?xml-stylesheet type="text/css" href="chrome://global/skin"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
                  type="text/css"?>
 <!--
+The test case creates 3 <xul:browser>s, load entries into them, modify the
+documents, and then check if grouped history merge / back / forward all work
+with bfcache preserved.
+
+Related bugs:
 https://bugzilla.mozilla.org/show_bug.cgi?id=1276553
+https://bugzilla.mozilla.org/show_bug.cgi?id=1310768
 -->
-<window title="Mozilla Bug 1276553"
+<window title="Test GroupedSHistory"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="run();">
 
   <!-- test code goes here -->
   <script type="application/javascript">
   <![CDATA[
 
   const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
   Cu.import("resource://testing-common/TestUtils.jsm");
@@ -19,325 +25,614 @@ https://bugzilla.mozilla.org/show_bug.cg
   Cu.import("resource://gre/modules/Task.jsm");
   ContentTask.setTestScope(window.opener.wrappedJSObject);
 
   let imports = ['SimpleTest', 'SpecialPowers', 'ok', 'is', 'info'];
   for (let name of imports) {
     window[name] = window.opener.wrappedJSObject[name];
   }
 
-  /** Test for Bug 1276553 **/
   function run() {
     SpecialPowers.pushPrefEnv(
       {'set' : [[ 'browser.groupedhistory.enabled', true ]]})
-    .then(() => test(false))
+    // Since we're not going to use GroupedSHistory in non-10s or
+    // chrome-only URLs, we don't test for non-remote browsers anymore.
+    //
+    // As a note, there are 2 known issues in non-10s:
+    // 1. nsDocShell::InternalLoad could start before nsFrameLoader calls
+    // EvictAllContentViewers, and causes RestoreFromHistory fails.
+    // 2. If using sendAsyncMessage, messages may be queued in
+    // SameProcessMessageQueue, and cause "pagehide" being delivered to the
+    // message manager of background <xul:browser> after swap.
     .then(() => test(true))
     .then(() => {
       window.close();
       SimpleTest.finish();
     });
   }
 
   function test(remote) {
     let act, bg1, bg2;
     return Promise.resolve()
 
-    // create first browser with 1 entry (which will always be the active one)
-    .then(() => info('TEST-INFO | test create active browser, remote=' + remote))
-    .then(() => createBrowser('pen', remote))
-    .then(b => act = b)
-    .then(() => verifyBrowser(act, 'pen'       /* title */,
-                                    0          /* index */,
-                                    1          /* length */,
-                                    false      /* canGoBack */,
-                                    false      /* canGoForward */,
-                                    false      /* partial */ ))
-
     // create background browser 1 with 1 entry
-    .then(() => info('TEST-INFO | test create background browser 1, remote=' + remote))
+    .then(() => info('TEST-INFO | test create browser #1, remote=' + remote))
     .then(() => createBrowser('pineapple', remote))
     .then(b => bg1 = b)
     .then(() => verifyBrowser(bg1, 'pineapple' /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     false      /* canGoBack */,
                                     false      /* canGoForward */,
                                     false      /* partial */ ))
+    .then(() => addBfcachedText(bg1))
 
      // create background browser 2 with 2 entries
-    .then(() => info('TEST-INFO | test create background browser 2, remote=' + remote))
+    .then(() => info('TEST-INFO | test create browser #2, remote=' + remote))
     .then(() => createBrowser('apple', remote))
     .then(b => bg2 = b)
     .then(() => verifyBrowser(bg2, 'apple'     /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     false      /* canGoBack */,
                                     false      /* canGoForward */,
                                     false      /* partial */ ))
+    .then(() => addBfcachedText(bg2))
     .then(() => loadURI(bg2, getDummyHtml('pencil')))
     .then(() => verifyBrowser(bg2, 'pencil'    /* title */,
                                     1          /* index */,
                                     2          /* length */,
                                     true       /* canGoBack */,
                                     false      /* canGoForward */,
                                     false      /* partial */ ))
+    .then(() => addBfcachedText(bg2))
+
+    // create active browser with 1 entry
+    .then(() => info('TEST-INFO | test create browser #3, remote=' + remote))
+    .then(() => createBrowser('pen', remote, true))
+    .then(b => act = b)
+    .then(() => verifyBrowser(act, 'pen'       /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    false      /* partial */ ))
+    .then(() => addBfcachedText(act))
 
     // merge to 2 entries pen-pineapple
-    .then(() => info('TEST-INFO | test merge history, remote=' + remote))
-    .then(() => mergeHistory(act, bg1))
+    // act: pineapple
+    // bg1: pen
+    .then(() => info('TEST-INFO | test merge history #1, remote=' + remote))
+    .then(() => mergeHistory(act, bg1, 'pineapple'))
     .then(() => verifyBrowser(act, 'pineapple' /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     true       /* canGoBack */,
                                     false      /* canGoForward */,
                                     true       /* partial */,
                                     1          /* offset */,
-                                    2          /* globalLength */ ))
+                                    2          /* globalLength */,
+                                    true       /* bfcached */ ))
 
     // merge to 4 entries pen-pineapple-apple-pencil
-    .then(() => mergeHistory(act, bg2))
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test merge history #2, remote=' + remote))
+    .then(() => mergeHistory(act, bg2, 'pencil'))
     .then(() => verifyBrowser(act, 'pencil'    /* title */,
                                     1          /* index */,
                                     2          /* length */,
                                     true       /* canGoBack */,
                                     false      /* canGoForward */,
                                     true       /* partial */,
                                     2          /* offset */,
-                                    4          /* globalLength */ ))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
 
     // test go back
-    .then(() => info('TEST-INFO | test history go back, remote=' + remote))
-    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act)))
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test history go back #1, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), 'pencil', 'apple'))
     .then(() => verifyBrowser(act, 'apple'     /* title */,
                                     0          /* index */,
                                     2          /* length */,
                                     true       /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     2          /* offset */,
-                                    4          /* globalLength */ ))
-    // XXX The 2nd pageshow comes from reload as current index of the active
-    // partial history remains the same
-    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), true))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test go back
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test history go back #2, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), 'apple', 'pineapple', bg2))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test go back
+    // act: pen
+    // bg1: pineapple
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test history go back #3, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), 'pineapple', 'pen', bg1))
+    .then(() => verifyBrowser(act, 'pen'       /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    0          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test go forward
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test history go forward #1, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), 'pen', 'pineapple', bg1))
     .then(() => verifyBrowser(act, 'pineapple' /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     true       /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     1          /* offset */,
-                                    4          /* globalLength */ ))
-    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), true))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test go forward
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test history go forward #2, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), 'pineapple', 'apple', bg2))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test go forward
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test history go forward #3, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), 'apple', 'pencil'))
+    .then(() => verifyBrowser(act, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test goto index
+    // act: pen
+    // bg1: apple-pencil
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test history goto index #1, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 0), 'pencil', 'pen', bg1))
     .then(() => verifyBrowser(act, 'pen'       /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     false      /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     0          /* offset */,
-                                    4          /* globalLength */ ))
-
-    // test go forward
-    .then(() => info('TEST-INFO | test history go forward, remote=' + remote))
-    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), true))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test goto index
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test history goto index #2, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 2), 'pen', 'apple', bg1))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test goto index
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test history goto index #3, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 1), 'apple', 'pineapple', bg2))
     .then(() => verifyBrowser(act, 'pineapple' /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     true       /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     1          /* offset */,
-                                    4          /* globalLength */ ))
-    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), true))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test goto index
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test history goto index #4, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 3), 'pineapple', 'pencil', bg2))
+    .then(() => verifyBrowser(act, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test content go back
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test content history go back #1, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGoBack.bind(null, act), 'pencil', 'apple'))
     .then(() => verifyBrowser(act, 'apple'     /* title */,
                                     0          /* index */,
                                     2          /* length */,
                                     true       /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     2          /* offset */,
-                                    4          /* globalLength */ ))
-    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act)))
-    .then(() => verifyBrowser(act, 'pencil'    /* title */,
-                                    1          /* index */,
-                                    2          /* length */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test content go back
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test content history go back #2, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGoBack.bind(null, act), 'apple', 'pineapple', bg2))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
                                     true       /* canGoBack */,
-                                    false      /* canGoForward */,
+                                    true       /* canGoForward */,
                                     true       /* partial */,
-                                    2          /* offset */,
-                                    4          /* globalLength */ ))
-
-    // test goto index
-    .then(() => info('TEST-INFO | test history goto index, remote=' + remote))
-    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 0), true))
+                                    1          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test content go back
+    // act: pen
+    // bg1: pineapple
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test content history go back #3, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGoBack.bind(null, act), 'pineapple', 'pen', bg1))
     .then(() => verifyBrowser(act, 'pen'       /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     false      /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     0          /* offset */,
-                                    4          /* globalLength */ ))
-    // expect 2 pageshow since we're also changing mIndex of the partial history
-    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 2), true, 2))
-    .then(() => verifyBrowser(act, 'apple'     /* title */,
-                                    0          /* index */,
-                                    2          /* length */,
-                                    true       /* canGoBack */,
-                                    true       /* canGoForward */,
-                                    true       /* partial */,
-                                    2          /* offset */,
-                                    4          /* globalLength */ ))
-    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 1), true))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test content go forward
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test content history go forward #1, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGoForward.bind(null, act), 'pen', 'pineapple', bg1))
     .then(() => verifyBrowser(act, 'pineapple' /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     true       /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     1          /* offset */,
-                                    4          /* globalLength */ ))
-    // expect 2 pageshow since we're also changing mIndex of the partial history
-    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 3), true, 2))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test content go forward
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test content history go forward #2, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGoForward.bind(null, act), 'pineapple', 'apple', bg2))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test content go forward
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test content history go forward #3, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGoForward.bind(null, act), 'apple', 'pencil'))
     .then(() => verifyBrowser(act, 'pencil'    /* title */,
                                     1          /* index */,
                                     2          /* length */,
                                     true       /* canGoBack */,
                                     false      /* canGoForward */,
                                     true       /* partial */,
                                     2          /* offset */,
-                                    4          /* globalLength */ ))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
 
-    // test history change to 3 entries pen-pineapple-banana
-    .then(() => info('TEST-INFO | test history change, remote=' + remote))
-    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 1), true))
+    // test content go
+    // act: pen
+    // bg1: apple-pencil
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test content history go #1, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGo.bind(null, act, -3), 'pencil', 'pen', bg1))
+    .then(() => verifyBrowser(act, 'pen'       /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    0          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test content go
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test content history go #2, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGo.bind(null, act, 2), 'pen', 'apple', bg1))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test content go
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test content history go #3, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGo.bind(null, act, -1), 'apple', 'pineapple', bg2))
     .then(() => verifyBrowser(act, 'pineapple' /* title */,
                                     0          /* index */,
                                     1          /* length */,
                                     true       /* canGoBack */,
                                     true       /* canGoForward */,
                                     true       /* partial */,
                                     1          /* offset */,
-                                    4          /* globalLength */ ))
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+    // test content go
+    // act: apple-pencil
+    // bg1: pen
+    // bg2: pineapple
+    .then(() => info('TEST-INFO | test content history go #4, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, contentGo.bind(null, act, 2), 'pineapple', 'pencil', bg2))
+    .then(() => verifyBrowser(act, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // test history change to 3 entries pen-pineapple-banana
+    // act: pineapple
+    // bg1: pen
+    // bg2: apple-pencil
+    .then(() => info('TEST-INFO | test history change, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 1), 'pencil', 'pineapple', bg2))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    4          /* globalLength */,
+                                    true       /* bfcached */ ))
+
+    // act: pineapple-banana
+    // bg1: pen
+    // bg2: (removed from group)
     .then(() => loadURI(act, getDummyHtml('banana')))
     .then(() => verifyBrowser(act, 'banana'    /* title */,
                                     1          /* index */,
                                     2          /* length */,
                                     true       /* canGoBack */,
                                     false      /* canGoForward */,
                                     true       /* partial */,
                                     1          /* offset */,
                                     3          /* globalLength */ ))
   }
 
   function getDummyHtml(title) {
     return 'data:text/html;charset=UTF-8,' +
-     '<html><head><title>' + title + '</title></head></html>'
+     '<html><head><title>' + title + '</title><script>' +
+     'window.addEventListener("pageshow", e => document.dispatchEvent(' +
+     '  new CustomEvent("test:content-pageshow", {detail: document.title})));' +
+     'window.addEventListener("pagehide", e => document.dispatchEvent(' +
+     '  new CustomEvent("test:content-pagehide", {detail: document.title})));' +
+     'window.addEventListener("visibilitychange", e => document.dispatchEvent(' +
+     '  new CustomEvent("test:content-visibilitychange", {detail: ' +
+     '    {title: document.title, visibility: document.visibilityState}})));' +
+     '</script></head><body><h1>' + title + '</h1><p id="p1"></p></body></html>';
   }
 
-  function createBrowser(title, remote) {
+  let gBrowserCount = 0;
+  function createBrowser(title, remote, active = false) {
     let browser = document.createElement('browser');
+    browser.testId = ++gBrowserCount; // for debugging
     browser.setAttribute('type', 'content');
     browser.setAttribute('remote', remote);
     browser.setAttribute('src', getDummyHtml(title));
+    browser.docShellIsActive = active;
     document.getElementById('stack').appendChild(browser);
     return BrowserTestUtils.browserLoaded(browser)
            .then(() => {
+             // Register our own event listeners.
+             //
+             // We don't use BrowserTestUtils.waitForContentEvents, because when
+             // swapping frameloaders, the event listeners at content side are
+             // also swapped to another browser. The message listener registered
+             // by waitForContentEvents will never receive messages consequently.
              browser.messageManager.loadFrameScript('data:,' +
-               'addEventListener("pageshow", () => sendAsyncMessage("test:pageshow", null), false);' +
-               'addEventListener("pagehide", () => sendAsyncMessage("test:pagehide", null), false);',
+               'addEventListener("test:content-pageshow", e => {' +
+                 'if (e.target == content.document) {' +
+                   'sendAsyncMessage("test:content-pageshow", { title: e.detail });' +
+                 '}' +
+               '}, true, true);' +
+               'addEventListener("test:content-pagehide", e => {' +
+                 'if (e.target == content.document) {' +
+                   'sendAsyncMessage("test:content-pagehide", { title: e.detail });' +
+                 '}' +
+               '}, true, true);' +
+               'addEventListener("test:content-visibilitychange", e => {' +
+                 'sendAsyncMessage("test:content-visibilitychange", e.detail);' +
+               '}, true, true);',
                true);
+
+             // Log for debugging purpose.
+             browser.messageManager.addMessageListener('test:content-pageshow',
+               msg => info('TEST-INFO | pageshow#' + browser.testId + ' ' + JSON.stringify(msg.data)));
+             browser.messageManager.addMessageListener('test:content-pagehide',
+               msg => info('TEST-INFO | pagehide#' + browser.testId + ' ' + JSON.stringify(msg.data)));
+             browser.messageManager.addMessageListener('test:content-visibilitychange',
+               msg => info('TEST-INFO | visibilitychange#' + browser.testId + ' ' + JSON.stringify(msg.data)));
            })
            .then(() => {
-             // a trick to ensure webProgress object is created for e10s case
+             // A trick to ensure webProgress object is created for e10s case.
+             // Otherwise canGoBack / canGoForward won't be updated.
              ok(browser.webProgress, 'check browser.webProgress exists');
              return browser;
            });
   }
 
+  function addBfcachedText(browser) {
+    return ContentTask.spawn(browser, null, () => {
+      content.document.getElementById('p1').textContent = 'modified';
+    });
+  }
+
   function loadURI(browser, uri) {
-    let promise = BrowserTestUtils.browserLoaded(browser, false, uri);
+    let promise = BrowserTestUtils.browserLoaded(browser, false);
     browser.loadURI(uri);
     return promise;
   }
 
-  function mergeHistory(b1, b2) {
+  function mergeHistory(b1, b2, title) {
     let promises = [];
     let pagehide1, pagehide2;
 
-    // For swapping there should be a pagehide followed by a pageshow.
-    promises.push(BrowserTestUtils.waitForMessage(b1.messageManager, 'test:pagehide', msg => pagehide1 = true));
-    promises.push(BrowserTestUtils.waitForMessage(b2.messageManager, 'test:pagehide', msg => pagehide2 = true));
-    promises.push(BrowserTestUtils.waitForMessage(b1.messageManager, 'test:pageshow', msg => pagehide1));
-    promises.push(BrowserTestUtils.waitForMessage(b2.messageManager, 'test:pageshow', msg => pagehide2));
+    promises.push(BrowserTestUtils.waitForMessage(b1.messageManager,
+      'test:content-visibilitychange', msg => msg.data &&
+        (msg.data.title == title) && (msg.data.visibility == 'visible')));
 
     // For swapping remote browsers, we'll also receive Content:LocationChange
     if (b1.isRemoteBrowser) {
+      // It's guaranteed location change can be delivered to b1's message
+      // manager, since frameloader is synchronously swapping on main thread
+      // after calling PartialSHistory::OnActive(). Therefore the message could
+      // only be handled after swapping.
       promises.push(BrowserTestUtils.waitForMessage(b1.messageManager, 'Content:LocationChange'));
     }
 
     promises.push(Promise.resolve().then(() => {
       let f1 = b1.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
       let f2 = b2.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
-      f1.appendPartialSessionHistoryAndSwap(f2);
+      return f1.appendPartialSessionHistoryAndSwap(f2);
     }));
 
     return Promise.all(promises);
   }
 
-  function wrapHistoryNavFn(browser, navFn, expectSwap = false, expectPageshowCount = 1) {
+  function wrapHistoryNavFn(browser, navFn, prevTitle, nextTitle, browserToSwap) {
     let promises = [];
-    let pagehide = false;
-    let pageshowCount = 0;
-
-    if (expectSwap) {
-      // For swapping there should be a pagehide followed by a pageshow.
-      promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
-        'test:pagehide', msg => pagehide = true));
 
-      // For swapping remote browsers, we'll also receive Content:LocationChange
-      if (browser.isRemoteBrowser) {
-        promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
-          'Content:LocationChange'));
-      }
-    }
+    // Expecting pagehide from previous page, and a pageshow from next page.
+    //
+    // "Pagehide" is sent before calling OnRequestCrossBrowserNavigation(),
+    // so it should be handled before swapping. "Pageshow" on the other hand
+    // should be handled after swapping cause frameloader is synchronously
+    // swapping on main thread after calling PartialSHistory::OnActive().
+    //
+    // Therefore both messages should be delivered to browser.messageManager.
     promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
-      'test:pageshow', msg => {
-        // Only count events after pagehide for swapping case.
-        if (!expectSwap || pagehide) {
-          return !--expectPageshowCount;
-        }
-        return false;
-      }));
+      'test:content-pagehide', msg => msg.data && (msg.data.title == prevTitle)));
+    promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
+      'test:content-pageshow', msg => msg.data && (msg.data.title == nextTitle)));
     promises.push(Task.spawn(navFn));
 
+    // For swapping remote browsers, we'll also receive Content:LocationChange
+    if (browserToSwap && browser.isRemoteBrowser) {
+      promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
+        'Content:LocationChange'));
+    }
+
     return Promise.all(promises);
   }
 
+  function contentGoBack(browser) {
+    ContentTask.spawn(browser, null, () => content.history.back());
+  }
+
+  function contentGoForward(browser) {
+    ContentTask.spawn(browser, null, () => content.history.forward());
+  }
+
+  function contentGo(browser, offset) {
+    ContentTask.spawn(browser, { offset },
+      ({ offset }) => content.history.go(offset));
+  }
+
   function verifyBrowser(browser, title, index, length, canGoBack, canGoForward,
-                         partial, offset = 0, globalLength = length) {
+                         partial, offset = 0, globalLength = length, bfcached = false) {
     is(browser.canGoBack, canGoBack, 'check browser.canGoBack');
     is(browser.canGoForward, canGoForward, 'check browser.canGoForward');
     if (partial) {
       let frameLoader = browser.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
       is(frameLoader.groupedSessionHistory.count, globalLength, 'check groupedSHistory.count');
     }
 
     return ContentTask.spawn(browser,
-                             { title, index, length, canGoBack, canGoForward, partial, offset, globalLength },
-                             ({ title, index, length, canGoBack, canGoForward, partial, offset, globalLength }) => {
+        { title, index, length, canGoBack, canGoForward, partial, offset, globalLength, bfcached },
+        ({ title, index, length, canGoBack, canGoForward, partial, offset, globalLength, bfcached }) => {
       let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
       let shistory = webNav.sessionHistory;
-      is(content.document.title, title, 'check title');
       is(webNav.canGoBack, canGoBack, 'check webNav.canGoBack');
       is(webNav.canGoForward, canGoForward, 'check webNav.canGoForward');
       is(shistory.index, index, 'check shistory.index');
       is(shistory.count, length, 'check shistory.count');
       is(shistory.isPartial, partial, 'check shistory.isPartial');
       is(shistory.globalIndexOffset, offset, 'check shistory.globalIndexOffset');
       is(shistory.globalCount, globalLength, 'check shistory.globalCount');
+      is(content.document.title, title, 'check title');
+      is(content.document.getElementById('p1').textContent, bfcached ? 'modified' : '', 'check bfcached content');
+      is(content.history.length, globalLength, 'check history.legnth');
     });
   }
 
   ]]>
   </script>
   <stack id="stack" flex="1" />
 </window>