Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 05 May 2015 14:45:52 -0700
changeset 273749 be81549901e24889bed56d6764ed16abebe48d91
parent 273731 e3d3f273cf88a894ae25f096ab85f68e49b1982a (current diff)
parent 273748 566f3ab3a23310453a13ef78ffe57e27dc32d7ea (diff)
child 273755 5907a8eca521ae88ca9caa7cba13346a8d0c16b7
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.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 fx-team to m-c a=merge
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -261,16 +261,20 @@
       <menuitem id="context-sharepage"
                 label="&sharePageCmd.label;"
                 accesskey="&sharePageCmd.accesskey;"
                 oncommand="SocialShare.sharePage();"/>
       <menuitem id="context-savepage"
                 label="&savePageCmd.label;"
                 accesskey="&savePageCmd.accesskey2;"
                 oncommand="gContextMenu.savePageAs();"/>
+      <menuitem id="context-pocket"
+                label="&saveToPocketCmd.label;"
+                accesskey="&saveToPocketCmd.accesskey;"
+                oncommand="gContextMenu.saveToPocket();"/>
       <menu id="context-markpageMenu" label="&social.markpageMenu.label;"
             accesskey="&social.markpageMenu.accesskey;">
         <menupopup/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
--- a/browser/base/content/browser-doctype.inc
+++ b/browser/base/content/browser-doctype.inc
@@ -1,13 +1,15 @@
 <!DOCTYPE window [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
 %brandDTD;
 <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" >
 %browserDTD;
+<!ENTITY % browserPocketDTD SYSTEM "chrome://browser/content/browser-pocket.dtd" >
+%browserPocketDTD;
 <!ENTITY % baseMenuDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd" >
 %baseMenuDTD;
 <!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >
 %charsetDTD;
 <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" >
 %textcontextDTD;
 <!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd">
   %customizeToolbarDTD;
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -382,16 +382,17 @@
 #ifndef XP_MACOSX
                placespopup="true"
 #endif
                context="placesContext"
                openInTabs="children"
                oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);"
                onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
                onpopupshowing="BookmarkingUI.onMainMenuPopupShowing(event);
+                               BookmarkingUI.updatePocketItemVisibility('menu_');
                                if (!this.parentNode._placesView)
                                  new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');"
                tooltip="bhTooltip" popupsinherittooltip="true">
       <menuitem id="bookmarksShowAll"
                 label="&showAllBookmarks2.label;"
                 command="Browser:ShowAllBookmarks"
                 key="manBookmarkKb"/>
       <menuseparator id="organizeBookmarksSeparator"/>
@@ -455,16 +456,19 @@
           <menuitem id="viewReadingListSidebar" class="subviewbutton"
                     oncommand="SidebarUI.toggle('readingListSidebar');"
                     label="&readingList.showSidebar.label;">
             <observes element="readingListSidebar" attribute="checked"/>
           </menuitem>
         </menupopup>
       </menu>
 #endif
+      <menuseparator id="menu_pocketSeparator"/>
+      <menuitem id="menu_pocket" label="&pocketMenuitem.label;"
+                oncommand="openUILink(Pocket.listURL, event);"/>
       <menuseparator id="bookmarksMenuItemsSeparator"/>
       <!-- Bookmarks menu items -->
       <menuseparator builder="end"
                      class="hide-if-empty-places-result"/>
       <menuitem id="menu_unsortedBookmarks"
                 label="&unsortedBookmarksCmd.label;"
                 oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/>
     </menupopup>
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1560,16 +1560,22 @@ let BookmarkingUI = {
     this.broadcaster.setAttribute("label", this.broadcaster.getAttribute(label));
   },
 
   onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
     this._updateBookmarkPageMenuItem();
     PlacesCommandHook.updateBookmarkAllTabsCommand();
   },
 
+  updatePocketItemVisibility: function BUI_updatePocketItemVisibility(prefix) {
+    let hidden = !CustomizableUI.getPlacementOfWidget("pocket-button");
+    document.getElementById(prefix + "pocket").hidden = hidden;
+    document.getElementById(prefix + "pocketSeparator").hidden = hidden;
+  },
+
   _showBookmarkedNotification: function BUI_showBookmarkedNotification() {
     function getCenteringTransformForRects(rectToPosition, referenceRect) {
       let topDiff = referenceRect.top - rectToPosition.top;
       let leftDiff = referenceRect.left - rectToPosition.left;
       let heightDiff = referenceRect.height - rectToPosition.height;
       let widthDiff = referenceRect.width - rectToPosition.width;
       return [(leftDiff + .5 * widthDiff) + "px", (topDiff + .5 * heightDiff) + "px"];
     }
@@ -1677,16 +1683,17 @@ let BookmarkingUI = {
       case "ViewHiding":
         this.onPanelMenuViewHiding(aEvent);
         break;
     }
   },
 
   onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
     this._updateBookmarkPageMenuItem();
+    this.updatePocketItemVisibility("panelMenu_");
     // Update checked status of the toolbar toggle.
     let viewToolbar = document.getElementById("panelMenu_viewBookmarksToolbar");
     let personalToolbar = document.getElementById("PersonalToolbar");
     if (personalToolbar.collapsed)
       viewToolbar.removeAttribute("checked");
     else
       viewToolbar.setAttribute("checked", "true");
     // Get all statically placed buttons to supply them with keyboard shortcuts.
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-pocket.dtd
@@ -0,0 +1,10 @@
+<!-- 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/. -->
+
+<!-- This is a temporary file and not meant for localization; later versions
+   - of Firefox include these strings in browser.dtd -->
+
+<!ENTITY saveToPocketCmd.label     "Save Page to Pocket">
+<!ENTITY saveToPocketCmd.accesskey "k">
+<!ENTITY pocketMenuitem.label      "View Pocket List">
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -878,16 +878,17 @@
           <menupopup id="BMB_bookmarksPopup"
                      class="cui-widget-panel cui-widget-panelview cui-widget-panelWithFooter PanelUI-subView"
                      placespopup="true"
                      context="placesContext"
                      openInTabs="children"
                      oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);"
                      onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
                      onpopupshowing="BookmarkingUI.onPopupShowing(event);
+                                     BookmarkingUI.updatePocketItemVisibility('BMB_');
                                      BookmarkingUI.attachPlacesView(event, this);"
                      tooltip="bhTooltip" popupsinherittooltip="true">
             <menuitem id="BMB_viewBookmarksSidebar"
                       class="subviewbutton"
                       label="&viewBookmarksSidebar2.label;"
                       type="checkbox"
                       oncommand="SidebarUI.toggle('viewBookmarksSidebar');">
               <observes element="viewBookmarksSidebar" attribute="checked"/>
@@ -965,16 +966,21 @@
               <menupopup id="BMB_readingListPopup"
                          placespopup="true"
                          onpopupshowing="ReadingListUI.onReadingListPopupShowing(this);">
                 <menuitem id="BMB_viewReadingListSidebar" class="subviewbutton"
                           oncommand="SidebarUI.show('readingListSidebar');"
                           label="&readingList.showSidebar.label;"/>
               </menupopup>
             </menu>
+            <menuseparator id="BMB_pocketSeparator"/>
+            <menuitem id="BMB_pocket"
+                      class="menuitem-iconic bookmark-item subviewbutton"
+                      label="&pocketMenuitem.label;"
+                      oncommand="openUILink(Pocket.listURL, event);"/>
             <menuseparator/>
             <!-- Bookmarks menu items will go here -->
             <menuitem id="BMB_bookmarksShowAll"
                       class="subviewbutton panel-subview-footer"
                       label="&showAllBookmarks2.label;"
                       command="Browser:ShowAllBookmarks"
                       key="manBookmarkKb"/>
           </menupopup>
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -172,16 +172,27 @@ nsContextMenu.prototype = {
     // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed
     // to be already loaded, since we load it on startup in nsBrowserGlue,
     // and CastingApps isn't, so check SimpleServiceDiscovery.services first
     // to avoid needing to load CastingApps.jsm if we don't need to.
     shouldShowCast = shouldShowCast && this.mediaURL &&
                      SimpleServiceDiscovery.services.length > 0 &&
                      CastingApps.getServicesForVideo(this.target).length > 0;
     this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
+
+    let canPocket = false;
+    if (shouldShow && window.gBrowser &&
+        this.browser.getTabBrowser() == window.gBrowser) {
+      let uri = this.browser.currentURI;
+      canPocket =
+        CustomizableUI.getPlacementOfWidget("pocket-button") &&
+        (uri.schemeIs("http") || uri.schemeIs("https") ||
+         (uri.schemeIs("about") && ReaderMode.getOriginalUrl(uri.spec)));
+    }
+    this.showItem("context-pocket", canPocket && window.Pocket && Pocket.isLoggedIn);
   },
 
   initViewItems: function CM_initViewItems() {
     // View source is always OK, unless in directory listing.
     this.showItem("context-viewpartialsource-selection",
                   this.isContentSelected);
     this.showItem("context-viewpartialsource-mathml",
                   this.onMathML && !this.isContentSelected);
@@ -1616,16 +1627,32 @@ nsContextMenu.prototype = {
   shareSelect: function CM_shareSelect() {
     SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: this.textSelected }, this.target);
   },
 
   savePageAs: function CM_savePageAs() {
     saveDocument(this.browser.contentDocumentAsCPOW);
   },
 
+  saveToPocket: function CM_saveToPocket() {
+    let pocketWidget = document.getElementById("pocket-button");
+    let placement = CustomizableUI.getPlacementOfWidget("pocket-button");
+    if (!placement)
+      return;
+
+    if (placement.area == CustomizableUI.AREA_PANEL) {
+      PanelUI.show().then(function() {
+        pocketWidget = document.getElementById("pocket-button");
+        pocketWidget.doCommand();
+      });
+    } else {
+      pocketWidget.doCommand();
+    }
+  },
+
   printFrame: function CM_printFrame() {
     PrintUtils.print(this.target.ownerDocument.defaultView, this.browser);
   },
 
   switchPageDirection: function CM_switchPageDirection() {
     this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
   },
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1499,22 +1499,18 @@
             let index = tab._tPos;
             let filter = this.mTabFilters[index];
             aBrowser.webProgress.removeProgressListener(filter);
             // Make sure the browser is destroyed so it unregisters from observer notifications
             aBrowser.destroy();
 
             // Change the "remote" attribute.
             let parent = aBrowser.parentNode;
-            let permanentKey = aBrowser.permanentKey;
             parent.removeChild(aBrowser);
             aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
-            // Tearing down the browser gives a new permanentKey but we want to
-            // keep the old one. Re-set it explicitly after unbinding from DOM.
-            aBrowser._permanentKey = permanentKey;
             parent.appendChild(aBrowser);
 
             // Restore the progress listener.
             aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
 
             if (aShouldBeRemote) {
               // Switching the browser to be remote will connect to a new child
               // process so the browser can no longer be considered to be
@@ -1623,16 +1619,17 @@
           <![CDATA[
             const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
             let remote = aParams && aParams.remote;
             let uriIsAboutBlank = aParams && aParams.uriIsAboutBlank;
             let isPreloadBrowser = aParams && aParams.isPreloadBrowser;
 
             let b = document.createElementNS(NS_XUL, "browser");
+            b.permanentKey = {};
             b.setAttribute("type", "content-targetable");
             b.setAttribute("message", "true");
             b.setAttribute("messagemanagergroup", "browsers");
             b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
             b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
 
             if (remote)
               b.setAttribute("remote", "true");
@@ -2486,16 +2483,21 @@
             filter.removeProgressListener(tabListener);
 
             // Make sure to unregister any open URIs.
             this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
 
             // Swap the docshells
             ourBrowser.swapDocShells(aOtherBrowser);
 
+            // Swap permanentKey properties.
+            let ourPermanentKey = ourBrowser.permanentKey;
+            ourBrowser.permanentKey = aOtherBrowser.permanentKey;
+            aOtherBrowser.permanentKey = ourPermanentKey;
+
             // Restore the progress listener
             this.mTabListeners[index] = tabListener =
               this.mTabProgressListener(aOurTab, ourBrowser, false);
 
             const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
             filter.addProgressListener(tabListener, notifyAll);
             ourBrowser.webProgress.addProgressListener(filter, notifyAll);
           ]]>
@@ -3871,16 +3873,17 @@
           }
         ]]></body>
       </method>
 
       <constructor>
         <![CDATA[
           let browserStack = document.getAnonymousElementByAttribute(this, "anonid", "browserStack");
           this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser");
+          this.mCurrentBrowser.permanentKey = {};
 
           this.mCurrentTab = this.tabContainer.firstChild;
           const nsIEventListenerService =
             Components.interfaces.nsIEventListenerService;
           let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
                               .getService(nsIEventListenerService);
           els.addSystemEventListener(document, "keydown", this, false);
           if (this.AppConstants.platform == "macosx") {
--- a/browser/base/content/test/general/browser_bug580956.js
+++ b/browser/base/content/test/general/browser_bug580956.js
@@ -14,14 +14,14 @@ function test() {
   is(numClosedTabs(), 0, "There should be 0 closed tabs.");
   ok(!isUndoCloseEnabled(), "Undo Close Tab should be disabled.");
 
   var tab = gBrowser.addTab("http://mochi.test:8888/");
   var browser = gBrowser.getBrowserForTab(tab);
   browser.addEventListener("load", function() {
     browser.removeEventListener("load", arguments.callee, true);
 
-    gBrowser.removeTab(tab);
-    ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled.");
-
-    finish();
+    BrowserTestUtils.removeTab(tab).then(() => {
+      ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled.");
+      finish();
+    });
   }, true);
 }
--- a/browser/base/content/test/general/browser_bug817947.js
+++ b/browser/base/content/test/general/browser_bug817947.js
@@ -29,24 +29,25 @@ function test() {
     });
   });
 }
 
 function preparePendingTab(aCallback) {
   let tab = gBrowser.addTab(URL);
 
   whenLoaded(tab.linkedBrowser, function () {
-    gBrowser.removeTab(tab);
-    let [{state}] = JSON.parse(SessionStore.getClosedTabData(window));
+    BrowserTestUtils.removeTab(tab).then(() => {
+      let [{state}] = JSON.parse(SessionStore.getClosedTabData(window));
 
-    tab = gBrowser.addTab("about:blank");
-    whenLoaded(tab.linkedBrowser, function () {
-      SessionStore.setTabState(tab, JSON.stringify(state));
-      ok(tab.hasAttribute("pending"), "tab should be pending");
-      aCallback(tab);
+      tab = gBrowser.addTab("about:blank");
+      whenLoaded(tab.linkedBrowser, function () {
+        SessionStore.setTabState(tab, JSON.stringify(state));
+        ok(tab.hasAttribute("pending"), "tab should be pending");
+        aCallback(tab);
+      });
     });
   });
 }
 
 function whenLoaded(aElement, aCallback) {
   aElement.addEventListener("load", function onLoad() {
     aElement.removeEventListener("load", onLoad, true);
     executeSoon(aCallback);
--- a/browser/base/content/test/general/browser_tab_detach_restore.js
+++ b/browser/base/content/test/general/browser_tab_detach_restore.js
@@ -5,16 +5,17 @@ add_task(function*() {
 
   // Clear out the closed windows set to start
   while (SessionStore.getClosedWindowCount() > 0)
     SessionStore.forgetClosedWindow(0);
 
   let tab = gBrowser.addTab();
   tab.linkedBrowser.loadURI(uri);
   yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  TabState.flush(tab.linkedBrowser);
 
   let key = tab.linkedBrowser.permanentKey;
   let win = gBrowser.replaceTabWithWindow(tab);
   yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
 
   is(win.gBrowser.selectedBrowser.permanentKey, key, "Should have properly copied the permanentKey");
   yield promiseWindowClosed(win);
 
--- a/browser/base/content/web-panels.xul
+++ b/browser/base/content/web-panels.xul
@@ -7,16 +7,18 @@
 
 <?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> 
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 <?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
 
 <!DOCTYPE page [
 <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
 %browserDTD;
+<!ENTITY % browserPocketDTD SYSTEM "chrome://browser/content/browser-pocket.dtd">
+%browserPocketDTD;
 <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
 %textcontextDTD;
 ]>
 
 <page id="webpanels-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="load()" onunload="unload()">
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -71,16 +71,17 @@ browser.jar:
         content/browser/aboutProviderDirectory.xhtml  (content/aboutProviderDirectory.xhtml)
         content/browser/aboutTabCrashed.css           (content/aboutTabCrashed.css)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
         content/browser/browser-pocket.properties     (content/browser-pocket.properties)
+        content/browser/browser-pocket.dtd            (content/browser-pocket.dtd)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/tab-content.js                (content/tab-content.js)
         content/browser/content.js                    (content/content.js)
         content/browser/defaultthemes/1.footer.jpg    (content/defaultthemes/1.footer.jpg)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
         content/browser/defaultthemes/1.icon.jpg      (content/defaultthemes/1.icon.jpg)
         content/browser/defaultthemes/1.preview.jpg   (content/defaultthemes/1.preview.jpg)
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -132,16 +132,20 @@
         <toolbarbutton id="panelMenu_viewReadingListSidebar"
                        label="&readingList.showSidebar.label;"
                        class="subviewbutton"
                        key="key_readingListSidebar"
                        oncommand="SidebarUI.toggle('readingListSidebar'); PanelUI.hide();">
           <observes element="readingListSidebar" attribute="checked"/>
           <observes element="readingListSidebar" attribute="hidden"/>
         </toolbarbutton>
+        <toolbarseparator id="panelMenu_pocketSeparator"/>
+        <toolbarbutton id="panelMenu_pocket" label="&pocketMenuitem.label;"
+                       class="subviewbutton cui-withicon"
+                       oncommand="openUILink(Pocket.listURL, event);"/>
         <toolbarseparator class="small-separator"/>
         <toolbaritem id="panelMenu_bookmarksMenu"
                      orient="vertical"
                      smoothscroll="false"
                      onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      flatList="true"
                      tooltip="bhTooltip">
--- a/browser/components/loop/build-jsx
+++ b/browser/components/loop/build-jsx
@@ -1,49 +1,105 @@
 #! /usr/bin/env python
 
 import os
+import sys
 import re
 from distutils import spawn
 import subprocess
 from threading import Thread
 import argparse
 
+
 def find_react_version(lib_dir):
     "Finds the React library version number currently used."
     for filename in os.listdir(lib_dir):
         match = re.match(r"react-(.*)-prod\.js", filename)
         if (match and match.group(1)):
             return match.group(1)
     print 'Unable to find the current react version used in content.'
     print 'Please checked the %s directory.' % lib_dir
     exit(1)
 
+
+def append_arguments(array1, array2):
+    "Appends array2 onto the end of array1"
+    result = array1[:]
+    result.extend(array2)
+    return result
+
+
+def check_jsx(jsx_path):
+    "Checks to see if jsx is installed or not"
+    if jsx_path is None:
+        print 'You do not have the react-tools installed'
+        print 'Please do $ npm install -g react-tools and make sure it is available in PATH'
+        exit(1)
+
+
+def find_react_command():
+    "Searches for a jsx location and forms a runnable command"
+    if sys.platform != 'win32':
+        jsx_path = spawn.find_executable('jsx')
+        check_jsx(jsx_path)
+        return [jsx_path]
+
+    # Else windows.
+    def find_excutable_no_extension(fileName):
+        """
+            spawn.find_executable assumes a '.exe' extension on windows
+            something which jsx doesn't have...
+        """
+        paths = os.environ['PATH'].split(os.pathsep)
+        for path in paths:
+            file = os.path.join(path, fileName)
+            if os.path.isfile(file):
+                return path
+        return None
+
+    # jsx isn't a true windows executable, so the standard spawn
+    # processes get upset. Hence, we have to use node to run the
+    # jsx file direct.
+    node = spawn.find_executable('node')
+    if node is None:
+        print 'You do not have node installed, or it is not in your PATH'
+        exit(1)
+
+    # We need the jsx path to make node happy
+    jsx_path = find_excutable_no_extension('jsx')
+    check_jsx(jsx_path)
+
+    # This is what node really wants to run.
+    jsx_path = os.path.join(jsx_path,
+                            "node_modules", "react-tools", "bin", "jsx")
+
+    return [node, jsx_path]
+
+
 SHARED_LIBS_DIR=os.path.join(os.path.dirname(__file__), "content", "shared", "libs")
 REACT_VERSION=find_react_version(SHARED_LIBS_DIR)
 
 src_files = []  # files to be compiled
 
-# search for react-tools install
-jsx_path = spawn.find_executable('jsx')
-if jsx_path is None:
-    print 'You do not have the react-tools installed'
-    print 'Please do $ npm install -g react-tools'
-    exit(1)
+run_command = find_react_command()
 
-p = subprocess.Popen([jsx_path, '-V'],
-                     stdout=subprocess.PIPE,
-                     stderr=subprocess.STDOUT)
-for line in iter(p.stdout.readline, b''):
-    info = line.rstrip()
+if sys.platform == 'win32':
+    print 'Please ensure you are running react-tools version %s' % REACT_VERSION
+    print 'You may be already, but we are not currently able to detect it'
+else:
+    p = subprocess.Popen(append_arguments(run_command, ['-V']),
+                         stdout=subprocess.PIPE,
+                         stderr=subprocess.STDOUT)
+    for line in iter(p.stdout.readline, b''):
+        info = line.rstrip()
 
-if not info == REACT_VERSION:
-    print 'You have the wrong version of react-tools installed'
-    print 'Please use version %s' % REACT_VERSION
-    exit(1)
+    if not info == REACT_VERSION:
+        print 'You have the wrong version of react-tools installed'
+        print 'Please use version %s' % REACT_VERSION
+        exit(1)
 
 # parse the CLI arguments
 description = 'Loop build tool for JSX files. ' + \
               'Will scan entire loop directory and compile them in place. ' + \
               'Must be executed from browser/components/loop directory.'
 
 parser = argparse.ArgumentParser(description=description)
 parser.add_argument('--watch', '-w', action='store_true', help='continuous' +
@@ -58,17 +114,17 @@ for dirname, dirnames, filenames in os.w
     for filename in filenames:
         if '.jsx' == os.path.splitext(filename)[1]:  # (root, ext)
             src_files.append(filename)
             if dirname not in unique_jsx_dirs:
                 unique_jsx_dirs.append(dirname)
 
 
 def jsx_run_watcher(path):
-    subprocess.call(['jsx', '-w', '-x', 'jsx', path, path])
+    subprocess.call(append_arguments(run_command, ['-w', '-x', 'jsx', path, path]))
 
 
 def start_jsx_watcher_threads(dirs):
     """
         starts a thread with a jsx watch process
         for every dir in the dirs argument
     """
     threads = []
@@ -106,9 +162,9 @@ def check_file_packaging(srcs):
             print f + ' not in jar.mn file'
 
 check_file_packaging(src_files)
 
 if args.watch:
     start_jsx_watcher_threads(unique_jsx_dirs)
 else:
     for d in unique_jsx_dirs:
-        out = subprocess.call(['jsx', '-x', 'jsx', d, d])
+        out = subprocess.call(append_arguments(run_command, ['-x', 'jsx', d, d]))
--- a/browser/components/loop/standalone/package.json
+++ b/browser/components/loop/standalone/package.json
@@ -9,16 +9,16 @@
   "engines": {
     "node": "0.10.x",
     "npm": "1.3.x"
   },
   "dependencies": {},
   "devDependencies": {
     "eslint": "0.20.x",
     "eslint-plugin-react": "2.2.x",
-    "express": "3.x"
+    "express": "4.x"
   },
   "scripts": {
     "test": "make test",
     "start": "make runserver"
   },
   "license": "MPL-2.0"
 }
--- a/browser/components/loop/standalone/server.js
+++ b/browser/components/loop/standalone/server.js
@@ -8,29 +8,35 @@
  * writing, we're just bootstrapping the linting infrastructure.
  */
 /* eslint-disable no-path-concat,no-process-exit */
 
 var express = require('express');
 var app = express();
 
 var port = process.env.PORT || 3000;
-var loopServerPort = process.env.LOOP_SERVER_PORT || 5000;
 var feedbackApiUrl = process.env.LOOP_FEEDBACK_API_URL ||
                      "https://input.allizom.org/api/v1/feedback";
 var feedbackProductName = process.env.LOOP_FEEDBACK_PRODUCT_NAME || "Loop";
+var loopServerUrl = process.env.LOOP_SERVER_URL || "http://localhost:5000";
+
+// Remove trailing slashes as double slashes in the url can confuse the server
+// responses.
+if (loopServerUrl[loopServerUrl.length - 1] === "/") {
+  loopServerUrl = loopServerUrl.slice(0, -1);
+}
 
 function getConfigFile(req, res) {
   "use strict";
 
   res.set('Content-Type', 'text/javascript');
   res.send([
     "var loop = loop || {};",
     "loop.config = loop.config || {};",
-    "loop.config.serverUrl = 'http://localhost:" + loopServerPort + "/v0';",
+    "loop.config.serverUrl = '" + loopServerUrl + "/v0';",
     "loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';",
     "loop.config.feedbackProductName = '" + feedbackProductName + "';",
     // XXX Update with the real marketplace url once the FxOS Loop app is
     //     uploaded to the marketplace bug 1053424
     "loop.config.marketplaceUrl = 'http://fake-market.herokuapp.com/iframe-install.html'",
     "loop.config.downloadFirefoxUrl = 'https://www.mozilla.org/firefox/new/?scene=2&utm_source=hello.firefox.com&utm_medium=referral&utm_campaign=non-webrtc-browser#download-fx';",
     "loop.config.privacyWebsiteUrl = 'https://www.mozilla.org/privacy/firefox-hello/';",
     "loop.config.learnMoreUrl = 'https://www.mozilla.org/hello/';",
@@ -49,33 +55,36 @@ function getConfigFile(req, res) {
 app.get('/content/config.js', getConfigFile);
 app.get('/content/c/config.js', getConfigFile);
 
 // Various mappings to let us end up with:
 // /test - for the test files
 // /ui - for the ui showcase
 // /content - for the standalone files.
 
-app.use('/test', express.static(__dirname + '/../test'));
 app.use('/ui', express.static(__dirname + '/../ui'));
 
 // This exists exclusively for the unit tests. They are served the
 // whole loop/ directory structure and expect some files in the standalone directory.
 app.use('/standalone/content', express.static(__dirname + '/content'));
 
 // We load /content this from  both /content *and* /../content. The first one
 // does what we need for running in the github loop-client context, the second one
 // handles running in the hg repo under mozilla-central and is used so that the shared
 // files are in the right location.
 app.use('/content', express.static(__dirname + '/content'));
 app.use('/content', express.static(__dirname + '/../content'));
 // These two are based on the above, but handle call urls, that have a /c/ in them.
 app.use('/content/c', express.static(__dirname + '/content'));
 app.use('/content/c', express.static(__dirname + '/../content'));
 
+// Two lines for the same reason as /content above.
+app.use('/test', express.static(__dirname + '/test'));
+app.use('/test', express.static(__dirname + '/../test'));
+
 // As we don't have hashes on the urls, the best way to serve the index files
 // appears to be to be to closely filter the url and match appropriately.
 function serveIndex(req, res) {
   return res.sendfile(__dirname + '/content/index.html');
 }
 
 app.get(/^\/content\/[\w\-]+$/, serveIndex);
 app.get(/^\/content\/c\/[\w\-]+$/, serveIndex);
--- a/browser/components/loop/test/functional/config.py
+++ b/browser/components/loop/test/functional/config.py
@@ -1,13 +1,14 @@
 # Loop server configuration
 CONTENT_SERVER_PORT = 3001
 LOOP_SERVER_PORT = 5001
+LOOP_SERVER_URL = "http://localhost:" + str(LOOP_SERVER_PORT)
 FIREFOX_PREFERENCES = {
-    "loop.server": "http://localhost:" + str(LOOP_SERVER_PORT),
+    "loop.server": LOOP_SERVER_URL + "/v0",
     "browser.dom.window.dump.enabled": True,
     # Some more changes might be necesarry to have this working in offline mode
     "media.peerconnection.default_iceservers": "[]",
     "media.peerconnection.use_document_iceservers": False,
     "media.peerconnection.ice.loopback": True,
     "devtools.chrome.enabled": True,
     "devtools.debugger.prompt-connection": False,
     "devtools.debugger.remote-enabled": True,
--- a/browser/components/loop/test/functional/serversetup.py
+++ b/browser/components/loop/test/functional/serversetup.py
@@ -16,28 +16,28 @@ sys.path.append(os.path.dirname(__file__
 import hanging_threads
 from config import *
 
 CONTENT_SERVER_COMMAND = ["make", "runserver"]
 CONTENT_SERVER_ENV = os.environ.copy()
 # Set PORT so that it does not interfere with any other
 # development server that might be running
 CONTENT_SERVER_ENV.update({"PORT": str(CONTENT_SERVER_PORT),
-                           "LOOP_SERVER_PORT": str(LOOP_SERVER_PORT)})
+                           "LOOP_SERVER_URL": LOOP_SERVER_URL})
 
 ROOMS_WEB_APP_URL = "http://localhost:" + str(CONTENT_SERVER_PORT) + \
   "/content/{token}"
 
 LOOP_SERVER_COMMAND = ["make", "runserver"]
 LOOP_SERVER_ENV = os.environ.copy()
 # Set PORT so that it does not interfere with any other
 # development server that might be running
 LOOP_SERVER_ENV.update({"NODE_ENV": "dev",
                         "PORT": str(LOOP_SERVER_PORT),
-                        "SERVER_ADDRESS": "localhost:" + str(LOOP_SERVER_PORT),
+                        "SERVER_ADDRESS": LOOP_SERVER_URL,
                         "ROOMS_WEB_APP_URL": ROOMS_WEB_APP_URL})
 
 
 class LoopTestServers:
     def __init__(self):
         loop_server_location = os.environ.get('LOOP_SERVER')
         if loop_server_location.startswith("http"):
             FIREFOX_PREFERENCES["loop.server"] = loop_server_location
--- a/browser/components/pocket/Pocket.jsm
+++ b/browser/components/pocket/Pocket.jsm
@@ -24,16 +24,18 @@ let Pocket = {
   get isLoggedIn() {
     return !!this._accessToken;
   },
 
   prefBranch: Services.prefs.getBranch("browser.pocket.settings."),
 
   get hostname() Services.prefs.getCharPref("browser.pocket.hostname"),
 
+  get listURL() { return "https://" + Pocket.hostname; },
+
   get _accessToken() {
     let sessionId, accessToken;
     let cookies = Services.cookies.getCookiesFromHost(this.hostname);
     while (cookies.hasMoreElements()) {
       let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2);
       if (cookie.name == "ftv1")
         accessToken = cookie.value;
       else if (cookie.name == "fsv1")
--- a/browser/components/pocket/jar.mn
+++ b/browser/components/pocket/jar.mn
@@ -11,16 +11,18 @@ browser.jar:
   content/browser/pocket/panels/css/firasans.css        (panels/css/firasans.css)
   content/browser/pocket/panels/css/saved.css           (panels/css/saved.css)
   content/browser/pocket/panels/css/signup.css           (panels/css/signup.css)
   content/browser/pocket/panels/fonts/FiraSans-Regular.woff (panels/fonts/FiraSans-Regular.woff)
   content/browser/pocket/panels/img/pocketlogo@1x.png              (panels/img/pocketlogo@1x.png)
   content/browser/pocket/panels/img/pocketlogo@2x.png              (panels/img/pocketlogo@2x.png)
   content/browser/pocket/panels/img/pocketlogosolo@1x.png          (panels/img/pocketlogosolo@1x.png)
   content/browser/pocket/panels/img/pocketlogosolo@2x.png          (panels/img/pocketlogosolo@2x.png)
+  content/browser/pocket/panels/img/pocketmenuitem16.png           (panels/img/pocketmenuitem16.png)
+  content/browser/pocket/panels/img/pocketmenuitem16@2x.png        (panels/img/pocketmenuitem16@2x.png)
   content/browser/pocket/panels/img/pocketmultidevices@1x.png      (panels/img/pocketmultidevices@1x.png)
   content/browser/pocket/panels/img/pocketmultidevices@2x.png      (panels/img/pocketmultidevices@2x.png)
   content/browser/pocket/panels/img/signup_firefoxlogo@1x.png      (panels/img/signup_firefoxlogo@1x.png)
   content/browser/pocket/panels/img/signup_firefoxlogo@2x.png      (panels/img/signup_firefoxlogo@2x.png)
   content/browser/pocket/panels/img/signup_help@1x.png             (panels/img/signup_help@1x.png)
   content/browser/pocket/panels/img/signup_help@2x.png             (panels/img/signup_help@2x.png)
   content/browser/pocket/panels/img/tag_close@1x.png               (panels/img/tag_close@1x.png)
   content/browser/pocket/panels/img/tag_close@2x.png               (panels/img/tag_close@2x.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..80816e1b049b3a2ad0b96df00f5f74bd4cef5eb3
GIT binary patch
literal 278
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP*AeO
zHKHUqKdq!Zu_%?nF(p4KRlzeiF+DXXH8G{K@MNkDP|;#f7sn8b)5$;n|F>sWo$+w4
z^9>e-fBtIo4mdQX8OYVJ$uK-(Q~1~5`jLIb9mX0qABI+)31JM6xIG^6H{4**GH;w9
zF0_D)bwOLh28Ixw#v2A24n{1?(`GP=7f5Vkm=@uXBa@}qpu+v3#Yp3T*@KDt4L1uK
zdo=bi_?-%1<|%NI)%(LJrp@E=&qU#ps(8YzKpqDp4My9I3a#!;Zh1>gteF|&UUkMQ
TuG9GgbS{IZtDnm{r-UW|?o3yS
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ee341860adc7356379cb1c9a5353e2bca982ff75
GIT binary patch
literal 422
zc%17D@N?(olHy`uVBq!ia0vp^iXhCv1|-9u9Lfh$oCO|{#S9GG!XV7ZFl&wkP*AeO
zHKHUqKdq!Zu_%?nF(p4KRlzeiF+DXXH8G{K@MNkD0|TS6r;B4q#NoHo&k8mx2()S*
z6w+uE(m5D>Zb|~@v<X0#kcOgafMXjUhlO{WK!{k+5lQ~{4|;ajDfj<XjLHs*ev+>+
z@9kQ{M{b83U)>XU)+nW^9P7BaSwfr9qEyT&q)kGvgZt4GfkWDQ!bU&uc289mXpWB)
zG;L)sOfv6a5fEj1!)SO^ito=O35M=Hx2MRpv+0{md}71cp!#L$liMF77&RGZ{B1Uj
z7D;)yLt>HfcC!WUF}r;QW{X%#1-P%BsF1_hpj_-(ziHBm2m10U3};x+w!bhI(&$JI
zkS+Ey<4yO8Oy2T)#<sUJN);MD@%kB6JAaJXxvRY4cRl9;^Xc**+AX*eN}06_{!C)u
zwZ>FnRs74flG|75c)nqksdVA8^DB?<NK7rUUd{33*v@ZzqAPRvGDy07nZ4#y#w=hM
OGkCiCxvX<aXaWF)$(v;W
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -5,29 +5,31 @@
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 const ENGINE_FLAVOR = "text/x-moz-search-engine";
 
+document.addEventListener("Initialized", () => {
+  if (Services.prefs.getBoolPref("browser.search.showOneOffButtons"))
+    return;
+
+  document.getElementById("category-search").hidden = true;
+  if (document.location.hash == "#search")
+    document.location.hash = "";
+});
+
 var gEngineView = null;
 
 var gSearchPane = {
 
   init: function ()
   {
-    if (!Services.prefs.getBoolPref("browser.search.showOneOffButtons")) {
-      document.getElementById("category-search").hidden = true;
-      if (document.location.hash == "#search")
-        document.location.hash = "";
-      return;
-    }
-
     gEngineView = new EngineView(new EngineStore());
     document.getElementById("engineList").view = gEngineView;
     this.buildDefaultEngineDropDown();
 
     window.addEventListener("click", this, false);
     window.addEventListener("command", this, false);
     window.addEventListener("dragstart", this, false);
     window.addEventListener("keypress", this, false);
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -87,16 +87,19 @@ const NOTAB_MESSAGES = new Set([
 ]);
 
 // The list of messages we want to receive even during the short period after a
 // frame has been removed from the DOM and before its frame script has finished
 // unloading.
 const CLOSED_MESSAGES = new Set([
   // For a description see above.
   "SessionStore:crashedTabRevived",
+
+  // For a description see above.
+  "SessionStore:update",
 ]);
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
@@ -333,16 +336,25 @@ let SessionStoreInternal = {
   // For each <browser> element being restored, records the current epoch.
   _browserEpochs: new WeakMap(),
 
   // Any browsers that fires the oop-browser-crashed event gets stored in
   // here - that way we know which browsers to ignore messages from (until
   // they get restored).
   _crashedBrowsers: new WeakSet(),
 
+  // A map (xul:browser -> nsIFrameLoader) that maps a browser to the last
+  // associated frameLoader we heard about.
+  _lastKnownFrameLoader: new WeakMap(),
+
+  // A map (xul:browser -> object) that maps a browser associated with a
+  // recently closed tab to all its necessary state information we need to
+  // properly handle final update message.
+  _closedTabs: new WeakMap(),
+
   // whether a setBrowserState call is in progress
   _browserSetState: false,
 
   // time in milliseconds when the session was started (saved across sessions),
   // defaults to now if no session was restored or timestamp doesn't exist
   _sessionStartTime: Date.now(),
 
   // states for all currently opened windows
@@ -607,24 +619,74 @@ let SessionStoreInternal = {
                       `from a browser that has no tab`);
     }
 
     switch (aMessage.name) {
       case "SessionStore:setupSyncHandler":
         TabState.setSyncHandler(browser, aMessage.objects.handler);
         break;
       case "SessionStore:update":
+        // Ignore messages from <browser> elements that have crashed
+        // and not yet been revived.
         if (this._crashedBrowsers.has(browser.permanentKey)) {
-          // Ignore messages from <browser> elements that have crashed
-          // and not yet been revived.
           return;
         }
+
+        // |browser.frameLoader| might be empty if the browser was already
+        // destroyed and its tab removed. In that case we still have the last
+        // frameLoader we know about to compare.
+        let frameLoader = browser.frameLoader ||
+                          this._lastKnownFrameLoader.get(browser.permanentKey);
+
+        // If the message isn't targeting the latest frameLoader discard it.
+        if (frameLoader != aMessage.targetFrameLoader) {
+          return;
+        }
+
+        // Record telemetry measurements done in the child and update the tab's
+        // cached state. Mark the window as dirty and trigger a delayed write.
         this.recordTelemetry(aMessage.data.telemetry);
         TabState.update(browser, aMessage.data);
         this.saveStateDelayed(win);
+
+        // Handle any updates sent by the child after the tab was closed. This
+        // might be the final update as sent by the "unload" handler but also
+        // any async update message that was sent before the child unloaded.
+        if (this._closedTabs.has(browser.permanentKey)) {
+          let {closedTabs, tabData} = this._closedTabs.get(browser.permanentKey);
+
+          // Update the closed tab's state. This will be reflected in its
+          // window's list of closed tabs as that refers to the same object.
+          TabState.copyFromCache({linkedBrowser: browser}, tabData.state);
+
+          // Is this the tab's final message?
+          if (aMessage.data.isFinal) {
+            // We expect no further updates.
+            this._closedTabs.delete(browser.permanentKey);
+            // The tab state no longer needs this reference.
+            delete tabData.permanentKey;
+
+            // Determine whether the tab state is worth saving.
+            let shouldSave = this._shouldSaveTabState(tabData.state);
+            let index = closedTabs.indexOf(tabData);
+
+            if (shouldSave && index == -1) {
+              // If the tab state is worth saving and we didn't push it onto
+              // the list of closed tabs when it was closed (because we deemed
+              // the state not worth saving) then add it to the window's list
+              // of closed tabs now.
+              this.saveClosedTabData(closedTabs, tabData);
+            } else if (!shouldSave && index > -1) {
+              // Remove from the list of closed tabs. The update messages sent
+              // after the tab was closed changed enough state so that we no
+              // longer consider its data interesting enough to keep around.
+              this.removeClosedTabData(closedTabs, index);
+            }
+          }
+        }
         break;
       case "SessionStore:restoreHistoryComplete":
         if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
           // Notify the tabbrowser that the tab chrome has been restored.
           let tabData = browser.__SS_data;
 
           // wall-paper fix for bug 439675: make sure that the URL to be loaded
           // is always visible in the address bar
@@ -717,44 +779,52 @@ let SessionStoreInternal = {
   },
 
   /* ........ Window Event Handlers .............. */
 
   /**
    * Implement nsIDOMEventListener for handling various window and tab events
    */
   handleEvent: function ssi_handleEvent(aEvent) {
-    var win = aEvent.currentTarget.ownerDocument.defaultView;
+    let win = aEvent.currentTarget.ownerDocument.defaultView;
+    let target = aEvent.originalTarget;
     switch (aEvent.type) {
       case "TabOpen":
-        this.onTabAdd(win, aEvent.originalTarget);
+        this.onTabAdd(win, target);
         break;
       case "TabClose":
         // aEvent.detail determines if the tab was closed by moving to a different window
         if (!aEvent.detail)
-          this.onTabClose(win, aEvent.originalTarget);
-        this.onTabRemove(win, aEvent.originalTarget);
+          this.onTabClose(win, target);
+        this.onTabRemove(win, target);
         break;
       case "TabSelect":
         this.onTabSelect(win);
         break;
       case "TabShow":
-        this.onTabShow(win, aEvent.originalTarget);
+        this.onTabShow(win, target);
         break;
       case "TabHide":
-        this.onTabHide(win, aEvent.originalTarget);
+        this.onTabHide(win, target);
         break;
       case "TabPinned":
       case "TabUnpinned":
       case "SwapDocShells":
         this.saveStateDelayed(win);
         break;
       case "oop-browser-crashed":
-        this.onBrowserCrashed(win, aEvent.originalTarget);
+        this.onBrowserCrashed(win, target);
         break;
+      case "XULFrameLoaderCreated":
+        if (target.tagName == "browser" && target.frameLoader) {
+          this._lastKnownFrameLoader.set(target.permanentKey, target.frameLoader);
+        }
+        break;
+      default:
+        throw new Error(`unhandled event ${aEvent.type}?`);
     }
     this._clearRestoringWindows();
   },
 
   /**
    * Generate a unique window identifier
    * @return string
    *         A unique string to identify a window
@@ -938,16 +1008,19 @@ let SessionStoreInternal = {
     // add tab change listeners to all already existing tabs
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
       this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
     }
     // notification of tab add/remove/selection/show/hide
     TAB_EVENTS.forEach(function(aEvent) {
       tabbrowser.tabContainer.addEventListener(aEvent, this, true);
     }, this);
+
+    // Keep track of a browser's latest frameLoader.
+    aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
   },
 
   /**
    * Called right before a new browser window is shown.
    * @param aWindow
    *        Window reference
    */
   onBeforeBrowserWindowShown: function (aWindow) {
@@ -1041,16 +1114,18 @@ let SessionStoreInternal = {
     }
 
     var tabbrowser = aWindow.gBrowser;
 
     TAB_EVENTS.forEach(function(aEvent) {
       tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
     }, this);
 
+    aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this);
+
     let winData = this._windows[aWindow.__SSi];
 
     // Collect window data only when *not* closed during shutdown.
     if (RunState.isRunning) {
       // Flush all data queued in the content script before the window is gone.
       TabState.flushWindow(aWindow);
 
       // update all window data for a last time
@@ -1306,16 +1381,21 @@ let SessionStoreInternal = {
    *        Tab reference
    * @param aNoNotification
    *        bool Do not save state if we're updating an existing tab
    */
   onTabAdd: function ssi_onTabAdd(aWindow, aTab, aNoNotification) {
     let browser = aTab.linkedBrowser;
     browser.addEventListener("SwapDocShells", this);
     browser.addEventListener("oop-browser-crashed", this);
+
+    if (browser.frameLoader) {
+      this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
+    }
+
     if (!aNoNotification) {
       this.saveStateDelayed(aWindow);
     }
   },
 
   /**
    * remove listeners for a tab
    * @param aWindow
@@ -1360,45 +1440,115 @@ 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;
     }
 
-    // Flush all data queued in the content script before the tab is gone.
-    TabState.flush(aTab.linkedBrowser);
-
     // Get the latest data for this tab (generally, from the cache)
     let tabState = TabState.collect(aTab);
 
     // Don't save private tabs
     let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
     if (!isPrivateWindow && tabState.isPrivate) {
       return;
     }
 
-    // store closed-tab data for undo
+    // Store closed-tab data for undo.
+    let tabbrowser = aWindow.gBrowser;
+    let tabTitle = this._replaceLoadingTitle(aTab.label, tabbrowser, aTab);
+    let {permanentKey} = aTab.linkedBrowser;
+
+    let tabData = {
+      permanentKey,
+      state: tabState,
+      title: tabTitle,
+      image: tabbrowser.getIcon(aTab),
+      pos: aTab._tPos,
+      closedAt: Date.now()
+    };
+
+    let closedTabs = this._windows[aWindow.__SSi]._closedTabs;
+
+    // Determine whether the tab contains any information worth saving. Note
+    // that there might be pending state changes queued in the child that
+    // didn't reach the parent yet. If a tab is emptied before closing then we
+    // might still remove it from the list of closed tabs later.
     if (this._shouldSaveTabState(tabState)) {
-      let tabTitle = aTab.label;
-      let tabbrowser = aWindow.gBrowser;
-      tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
-
-      this._windows[aWindow.__SSi]._closedTabs.unshift({
-        state: tabState,
-        title: tabTitle,
-        image: tabbrowser.getIcon(aTab),
-        pos: aTab._tPos,
-        closedAt: Date.now()
-      });
-      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);
+      // Save the tab state, for now. We might push a valid tab out
+      // of the list but those cases should be extremely rare and
+      // do probably never occur when using the browser normally.
+      // (Tests or add-ons might do weird things though.)
+      this.saveClosedTabData(closedTabs, tabData);
+    }
+
+    // Remember the closed tab to properly handle any last updates included in
+    // the final "update" message sent by the frame script's unload handler.
+    this._closedTabs.set(permanentKey, {closedTabs, tabData});
+  },
+
+  /**
+   * Insert a given |tabData| object into the list of |closedTabs|. We will
+   * determine the right insertion point based on the .closedAt properties of
+   * all tabs already in the list. The list will be truncated to contain a
+   * maximum of |this._max_tabs_undo| entries.
+   *
+   * @param closedTabs (array)
+   *        The list of closed tabs for a window.
+   * @param tabData (object)
+   *        The tabData to be inserted.
+   */
+  saveClosedTabData(closedTabs, tabData) {
+    // Find the index of the first tab in the list
+    // of closed tabs that was closed before our tab.
+    let index = closedTabs.findIndex(tab => {
+      return tab.closedAt < tabData.closedAt;
+    });
+
+    // If we found no tab closed before our
+    // tab then just append it to the list.
+    if (index == -1) {
+      index = closedTabs.length;
     }
+
+    // Insert tabData at the right position.
+    closedTabs.splice(index, 0, tabData);
+
+    // Truncate the list of closed tabs, if needed.
+    if (closedTabs.length > this._max_tabs_undo) {
+      closedTabs.splice(this._max_tabs_undo, closedTabs.length);
+    }
+  },
+
+  /**
+   * Remove the closed tab data at |index| from the list of |closedTabs|. If
+   * the tab's final message is still pending we will simply discard it when
+   * it arrives so that the tab doesn't reappear in the list.
+   *
+   * @param closedTabs (array)
+   *        The list of closed tabs for a window.
+   * @param index (uint)
+   *        The index of the tab to remove.
+   */
+  removeClosedTabData(closedTabs, index) {
+    // Remove the given index from the list.
+    let [closedTab] = closedTabs.splice(index, 1);
+
+    // If the closed tab's state still has a .permanentKey property then we
+    // haven't seen its final update message yet. Remove it from the map of
+    // closed tabs so that we will simply discard its last messages and will
+    // not add it back to the list of closed tabs again.
+    if (closedTab.permanentKey) {
+      this._closedTabs.delete(closedTab.permanentKey);
+      delete closedTab.permanentKey;
+    }
+
+    return closedTab;
   },
 
   /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
   onTabSelect: function ssi_onTabSelect(aWindow) {
@@ -1691,28 +1841,27 @@ let SessionStoreInternal = {
 
     // default to the most-recently closed tab
     aIndex = aIndex || 0;
     if (!(aIndex in closedTabs)) {
       throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
     }
 
     // fetch the data of closed tab, while removing it from the array
-    let [closedTab] = closedTabs.splice(aIndex, 1);
-    let closedTabState = closedTab.state;
+    let {state, pos} = this.removeClosedTabData(closedTabs, aIndex);
 
     // create a new tab
     let tabbrowser = aWindow.gBrowser;
     let tab = tabbrowser.selectedTab = tabbrowser.addTab();
 
     // restore tab content
-    this.restoreTab(tab, closedTabState);
+    this.restoreTab(tab, state);
 
     // restore the tab's position
-    tabbrowser.moveTabTo(tab, closedTab.pos);
+    tabbrowser.moveTabTo(tab, pos);
 
     // focus the tab's content area (bug 342432)
     tab.linkedBrowser.focus();
 
     return tab;
   },
 
   forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) {
@@ -1724,17 +1873,17 @@ let SessionStoreInternal = {
 
     // default to the most-recently closed tab
     aIndex = aIndex || 0;
     if (!(aIndex in closedTabs)) {
       throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
     }
 
     // remove closed tab from the array
-    closedTabs.splice(aIndex, 1);
+    this.removeClosedTabData(closedTabs, aIndex);
   },
 
   getClosedWindowCount: function ssi_getClosedWindowCount() {
     return this._closedWindows.length;
   },
 
   getClosedWindowData: function ssi_getClosedWindowData() {
     return JSON.stringify(this._closedWindows);
--- a/browser/components/sessionstore/TabState.jsm
+++ b/browser/components/sessionstore/TabState.jsm
@@ -48,16 +48,20 @@ this.TabState = Object.freeze({
   },
 
   collect: function (tab) {
     return TabStateInternal.collect(tab);
   },
 
   clone: function (tab) {
     return TabStateInternal.clone(tab);
+  },
+
+  copyFromCache: function (tab, tabData, options) {
+    TabStateInternal.copyFromCache(tab, tabData, options);
   }
 });
 
 let TabStateInternal = {
   // A map (xul:browser -> handler) that maps a tab to the
   // synchronous collection handler object for that tab.
   // See SyncHandler in content-sessionStore.js.
   _syncHandlers: new WeakMap(),
@@ -213,32 +217,32 @@ let TabStateInternal = {
 
     if (tab.__SS_extdata)
       tabData.extData = tab.__SS_extdata;
     else if (tabData.extData)
       delete tabData.extData;
 
     // Copy data from the tab state cache only if the tab has fully finished
     // restoring. We don't want to overwrite data contained in __SS_data.
-    this._copyFromCache(tab, tabData, options);
+    this.copyFromCache(tab, tabData, options);
 
     return tabData;
   },
 
   /**
    * Copy tab data for the given |tab| from the cache to |tabData|.
    *
    * @param tab (xul:tab)
    *        The tab belonging to the given |tabData| object.
    * @param tabData (object)
    *        The tab data belonging to the given |tab|.
    * @param options (object)
    *        {includePrivateData: true} to always include private data
    */
-  _copyFromCache: function (tab, tabData, options = {}) {
+  copyFromCache: function (tab, tabData, options = {}) {
     let data = TabStateCache.get(tab.linkedBrowser);
     if (!data) {
       return;
     }
 
     // The caller may explicitly request to omit privacy checks.
     let includePrivateData = options && options.includePrivateData;
 
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -111,16 +111,22 @@ let MessageListener = {
     "SessionStore:resetRestore",
   ],
 
   init: function () {
     this.MESSAGES.forEach(m => addMessageListener(m, this));
   },
 
   receiveMessage: function ({name, data}) {
+    // The docShell might be gone. Don't process messages,
+    // that will just lead to errors anyway.
+    if (!docShell) {
+      return;
+    }
+
     switch (name) {
       case "SessionStore:restoreHistory":
         this.restoreHistory(data);
         break;
       case "SessionStore:restoreTabContent":
         this.restoreTabContent(data);
         break;
       case "SessionStore:resetRestore":
@@ -217,17 +223,21 @@ let SyncHandler = {
 
   /**
    * DO NOT USE - DEBUGGING / TESTING ONLY
    *
    * This function is used to simulate certain situations where race conditions
    * can occur by sending data shortly before flushing synchronously.
    */
   flushAsync: function () {
-    MessageQueue.flushAsync();
+    if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) {
+      throw new Error("flushAsync() must be used for testing, only.");
+    }
+
+    MessageQueue.send();
   }
 };
 
 /**
  * Listens for changes to the session history. Whenever the user navigates
  * we will collect URLs and everything belonging to session history.
  *
  * Causes a SessionStore:update message to be sent that contains the current
@@ -675,19 +685,18 @@ let MessageQueue = {
 
     durationMs = Date.now() - durationMs;
     let telemetry = {
       FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs
     }
 
     // Send all data to the parent process.
     sendMessage("SessionStore:update", {
-      id: this._id,
-      data: data,
-      telemetry: telemetry
+      id: this._id, data, telemetry,
+      isFinal: options.isFinal || false
     });
 
     // Increase our unique message ID.
     this._id++;
   },
 
   /**
    * This function is used to make the message queue flush all queue data that
@@ -701,30 +710,16 @@ let MessageQueue = {
   flush: function (id) {
     // It's important to always send data, even if there is nothing to flush.
     // The update message will be received by the parent process that can then
     // update its last received update ID to ignore stale messages.
     this.send({id: id + 1, sync: true});
 
     this._data.clear();
     this._lastUpdated.clear();
-  },
-
-  /**
-   * DO NOT USE - DEBUGGING / TESTING ONLY
-   *
-   * This function is used to simulate certain situations where race conditions
-   * can occur by sending data shortly before flushing synchronously.
-   */
-  flushAsync: function () {
-    if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) {
-      throw new Error("flushAsync() must be used for testing, only.");
-    }
-
-    this.send();
   }
 };
 
 EventListener.init();
 MessageListener.init();
 FormDataListener.init();
 SyncHandler.init();
 PageStyleListener.init();
@@ -754,16 +749,20 @@ function handleRevivedTab() {
   }
 }
 
 // If we're browsing from the tab crashed UI to a blacklisted URI that keeps
 // this browser non-remote, we'll handle that in a pagehide event.
 addEventListener("pagehide", handleRevivedTab);
 
 addEventListener("unload", () => {
+  // Upon frameLoader destruction, send a final update message to
+  // the parent and flush all data currently held in the child.
+  MessageQueue.send({isFinal: true});
+
   // If we're browsing from the tab crashed UI to a URI that causes the tab
   // to go remote again, we catch this in the unload event handler, because
   // swapping out the non-remote browser for a remote one in
   // tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide
   // event to be fired.
   handleRevivedTab();
 
   // Remove all registered nsIObservers.
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -60,16 +60,18 @@ support-files =
 
 
 #disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
 #disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
 #disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
 
 [browser_aboutPrivateBrowsing.js]
 [browser_aboutSessionRestore.js]
+[browser_async_remove_tab.js]
+run-if = e10s
 [browser_attributes.js]
 [browser_backup_recovery.js]
 [browser_broadcast.js]
 [browser_capabilities.js]
 [browser_cleaner.js]
 [browser_cookies.js]
 [browser_crashedTabs.js]
 skip-if = !e10s || !crashreporter
--- a/browser/components/sessionstore/test/browser_350525.js
+++ b/browser/components/sessionstore/test/browser_350525.js
@@ -1,21 +1,21 @@
-function test() {
+"use strict";
+
+add_task(function* () {
   /** Test for Bug 350525 **/
 
   function test(aLambda) {
     try {
       return aLambda() || true;
     }
     catch (ex) { }
     return false;
   }
 
-  waitForExplicitFinish();
-
   ////////////////////////////
   // setWindowValue, et al. //
   ////////////////////////////
   let key = "Unique name: " + Date.now();
   let value = "Unique value: " + Math.random();
 
   // test adding
   ok(test(function() ss.setWindowValue(window, key, value)), "set a window value");
@@ -51,49 +51,46 @@ function test() {
 
   // value should not exist post-delete
   is(ss.getTabValue(tab, key), "", "tab value was deleted");
 
   // test deleting a non-existent value
   ok(test(function() ss.deleteTabValue(tab, key)), "delete non-existent tab value");
 
   // clean up
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   /////////////////////////////////////
   // getClosedTabCount, undoCloseTab //
   /////////////////////////////////////
 
   // get closed tab count
   let count = ss.getClosedTabCount(window);
   let max_tabs_undo = gPrefService.getIntPref("browser.sessionstore.max_tabs_undo");
   ok(0 <= count && count <= max_tabs_undo,
      "getClosedTabCount returns zero or at most max_tabs_undo");
 
   // create a new tab
   let testURL = "about:";
   tab = gBrowser.addTab(testURL);
-  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
-    // make sure that the next closed tab will increase getClosedTabCount
-    gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
+  yield promiseBrowserLoaded(tab.linkedBrowser);
 
-    // remove tab
-    gBrowser.removeTab(tab);
+  // make sure that the next closed tab will increase getClosedTabCount
+  gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
+  registerCleanupFunction(() => gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"));
 
-    // getClosedTabCount
-    var newcount = ss.getClosedTabCount(window);
-    ok(newcount > count, "after closing a tab, getClosedTabCount has been incremented");
+  // remove tab
+  yield promiseRemoveTab(tab);
 
-    // undoCloseTab
-    tab = test(function() ss.undoCloseTab(window, 0));
-    ok(tab, "undoCloseTab doesn't throw")
-
-    promiseTabRestored(tab).then(() => {
-      is(tab.linkedBrowser.currentURI.spec, testURL, "correct tab was reopened");
+  // getClosedTabCount
+  let newcount = ss.getClosedTabCount(window);
+  ok(newcount > count, "after closing a tab, getClosedTabCount has been incremented");
 
-      // clean up
-      if (gPrefService.prefHasUserValue("browser.sessionstore.max_tabs_undo"))
-        gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
-      gBrowser.removeTab(tab);
-      finish();
-    });
-  });
-}
+  // undoCloseTab
+  tab = test(function() ss.undoCloseTab(window, 0));
+  ok(tab, "undoCloseTab doesn't throw")
+
+  yield promiseTabRestored(tab);
+  is(tab.linkedBrowser.currentURI.spec, testURL, "correct tab was reopened");
+
+  // clean up
+  gBrowser.removeTab(tab);
+});
--- a/browser/components/sessionstore/test/browser_367052.js
+++ b/browser/components/sessionstore/test/browser_367052.js
@@ -1,37 +1,41 @@
 /* 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 test() {
-  /** Test for Bug 367052 **/
+"use strict";
 
-  waitForExplicitFinish();
-
+add_task(function* () {
   // make sure that the next closed tab will increase getClosedTabCount
   let max_tabs_undo = gPrefService.getIntPref("browser.sessionstore.max_tabs_undo");
   gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", max_tabs_undo + 1);
-  let closedTabCount = ss.getClosedTabCount(window);
+  registerCleanupFunction(() => gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"));
+
+  // Empty the list of closed tabs.
+  while (ss.getClosedTabCount(window)) {
+    ss.forgetClosedTab(window, 0);
+  }
 
   // restore a blank tab
   let tab = gBrowser.addTab("about:");
-  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
-    let history = tab.linkedBrowser.webNavigation.sessionHistory;
-    ok(history.count >= 1, "the new tab does have at least one history entry");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
 
-    promiseTabState(tab, {entries: []}).then(() => {
-      // We may have a different sessionHistory object if the tab
-      // switched from non-remote to remote.
-      history = tab.linkedBrowser.webNavigation.sessionHistory;
-      ok(history.count == 0, "the tab was restored without any history whatsoever");
+  let count = yield promiseSHistoryCount(tab.linkedBrowser);
+  ok(count >= 1, "the new tab does have at least one history entry");
+
+  yield promiseTabState(tab, {entries: []});
 
-      gBrowser.removeTab(tab);
-      ok(ss.getClosedTabCount(window) == closedTabCount,
-         "The closed blank tab wasn't added to Recently Closed Tabs");
+  // We may have a different sessionHistory object if the tab
+  // switched from non-remote to remote.
+  count = yield promiseSHistoryCount(tab.linkedBrowser);
+  is(count, 0, "the tab was restored without any history whatsoever");
 
-      // clean up
-      if (gPrefService.prefHasUserValue("browser.sessionstore.max_tabs_undo"))
-        gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
-      finish();
-    });
+  yield promiseRemoveTab(tab);
+  is(ss.getClosedTabCount(window), 0,
+     "The closed blank tab wasn't added to Recently Closed Tabs");
+});
+
+function promiseSHistoryCount(browser) {
+  return ContentTask.spawn(browser, null, function* () {
+    return docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.count;
   });
 }
--- a/browser/components/sessionstore/test/browser_394759_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_394759_perwindowpb.js
@@ -1,25 +1,21 @@
 /* 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/. */
 
-/** Private Browsing Test for Bug 394759 **/
-
-let closedWindowCount = 0;
-// Prevent VM timers issues, cache now and increment it manually.
-let now = Date.now();
+"use strict";
 
 const TESTS = [
   { url: "about:config",
     key: "bug 394759 Non-PB",
-    value: "uniq" + (++now) },
+    value: "uniq" + r() },
   { url: "about:mozilla",
     key: "bug 394759 PB",
-    value: "uniq" + (++now) },
+    value: "uniq" + r() },
 ];
 
 function promiseTestOpenCloseWindow(aIsPrivate, aTest) {
   return Task.spawn(function*() {
     let win = yield promiseNewWindowLoaded({ "private": aIsPrivate });
     win.gBrowser.selectedBrowser.loadURI(aTest.url);
     yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
     yield Promise.resolve();
@@ -28,63 +24,21 @@ function promiseTestOpenCloseWindow(aIsP
     // Close.
     yield promiseWindowClosed(win);
   });
 }
 
 function promiseTestOnWindow(aIsPrivate, aValue) {
   return Task.spawn(function*() {
     let win = yield promiseNewWindowLoaded({ "private": aIsPrivate });
-    yield promiseCheckClosedWindows(aIsPrivate, aValue);
-    registerCleanupFunction(() => promiseWindowClosed(win));
-  });
-}
-
-function promiseCheckClosedWindows(aIsPrivate, aValue) {
-  return Task.spawn(function*() {
     let data = JSON.parse(ss.getClosedWindowData())[0];
     is(ss.getClosedWindowCount(), 1, "Check that the closed window count hasn't changed");
     ok(JSON.stringify(data).indexOf(aValue) > -1,
        "Check the closed window data was stored correctly");
-  });
-}
-
-function promiseBlankState() {
-  return Task.spawn(function*() {
-    // Set interval to a large time so state won't be written while we setup
-    // environment.
-    Services.prefs.setIntPref("browser.sessionstore.interval", 100000);
-    registerCleanupFunction(() =>  Services.prefs.clearUserPref("browser.sessionstore.interval"));
-
-    // Set up the browser in a blank state. Popup windows in previous tests
-    // result in different states on different platforms.
-    let blankState = JSON.stringify({
-      windows: [{
-        tabs: [{ entries: [{ url: "about:blank" }] }],
-        _closedTabs: []
-      }],
-      _closedWindows: []
-    });
-
-    ss.setBrowserState(blankState);
-
-    // Wait for the sessionstore.js file to be written before going on.
-    // Note: we don't wait for the complete event, since if asyncCopy fails we
-    // would timeout.
-
-    yield forceSaveState();
-    closedWindowCount = ss.getClosedWindowCount();
-    is(closedWindowCount, 0, "Correctly set window count");
-
-    // Remove the sessionstore.js file before setting the interval to 0
-    yield SessionFile.wipe();
-
-    // Make sure that sessionstore.js can be forced to be created by setting
-    // the interval pref to 0.
-    yield forceSaveState();
+    registerCleanupFunction(() => promiseWindowClosed(win));
   });
 }
 
 add_task(function* init() {
   forgetClosedWindows();
   while (ss.getClosedTabCount(window) > 0) {
     ss.forgetClosedTab(window, 0);
   }
--- a/browser/components/sessionstore/test/browser_454908.js
+++ b/browser/components/sessionstore/test/browser_454908.js
@@ -23,17 +23,17 @@ add_task(function* test_dont_save_passwo
   yield promiseBrowserLoaded(browser);
 
   // Fill in some values.
   let usernameValue = "User " + Math.random();
   yield setInputValue(browser, {id: "username", value: usernameValue});
   yield setInputValue(browser, {id: "passwd", value: PASS});
 
   // Close and restore the tab.
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   tab = ss.undoCloseTab(window, 0);
   browser = tab.linkedBrowser;
   yield promiseTabRestored(tab);
 
   // Check that password fields aren't saved/restored.
   let username = yield getInputValue(browser, {id: "username"});
   is(username, usernameValue, "username was saved/restored");
   let passwd = yield getInputValue(browser, {id: "passwd"});
@@ -41,12 +41,11 @@ add_task(function* test_dont_save_passwo
 
   // Write to disk and read our file.
   yield forceSaveState();
   yield promiseForEachSessionRestoreFile((state, key) =>
     // Ensure that we have not saved our password.
     ok(!state.includes(PASS), "password has not been written to file " + key)
   );
 
-
   // Cleanup.
   gBrowser.removeTab(tab);
 });
--- a/browser/components/sessionstore/test/browser_456342.js
+++ b/browser/components/sessionstore/test/browser_456342.js
@@ -14,17 +14,17 @@ add_task(function test_restore_nonstanda
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Fill in form values.
   let expectedValue = Math.random();
   yield setFormElementValues(browser, {value: expectedValue});
 
   // Remove tab and check collected form data.
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   let undoItems = JSON.parse(ss.getClosedTabData(window));
   let savedFormData = undoItems[0].state.formdata;
 
   let countGood = 0, countBad = 0;
   for (let id of Object.keys(savedFormData.id)) {
     if (savedFormData.id[id] == expectedValue) {
       countGood++;
     } else {
--- a/browser/components/sessionstore/test/browser_579879.js
+++ b/browser/components/sessionstore/test/browser_579879.js
@@ -1,21 +1,20 @@
-function test() {
-  waitForExplicitFinish();
+"use strict";
 
-  var tab1 = gBrowser.addTab("data:text/plain;charset=utf-8,foo");
+add_task(function* () {
+  let tab1 = gBrowser.addTab("data:text/plain;charset=utf-8,foo");
   gBrowser.pinTab(tab1);
 
-  promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
-    var tab2 = gBrowser.addTab();
-    gBrowser.pinTab(tab2);
+  yield promiseBrowserLoaded(tab1.linkedBrowser);
+  let tab2 = gBrowser.addTab();
+  gBrowser.pinTab(tab2);
+
+  is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 is at the first position");
+  yield promiseRemoveTab(tab1);
 
-    is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 is at the first position");
-    gBrowser.removeTab(tab1);
-    tab1 = undoCloseTab();
-    ok(tab1.pinned, "pinned tab 1 has been restored as a pinned tab");
-    is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 has been restored to the first position");
+  tab1 = undoCloseTab();
+  ok(tab1.pinned, "pinned tab 1 has been restored as a pinned tab");
+  is(Array.indexOf(gBrowser.tabs, tab1), 0, "pinned tab 1 has been restored to the first position");
 
-    gBrowser.removeTab(tab1);
-    gBrowser.removeTab(tab2);
-    finish();
-  });
-}
+  gBrowser.removeTab(tab1);
+  gBrowser.removeTab(tab2);
+});
--- a/browser/components/sessionstore/test/browser_581937.js
+++ b/browser/components/sessionstore/test/browser_581937.js
@@ -1,36 +1,19 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
 // Tests that an about:blank tab with no history will not be saved into
 // session store and thus, it will not show up in Recently Closed Tabs.
 
-let tab;
-function test() {
-  waitForExplicitFinish();
-
-  gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", 0);
-  gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
+"use strict";
 
-  is(ss.getClosedTabCount(window), 0, "should be no closed tabs");
-
-  gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
-
-  tab = gBrowser.addTab();
-}
+add_task(function* () {
+  let tab = gBrowser.addTab("about:blank");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
 
-function onTabOpen(aEvent) {
-  gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
-
-  // Let other listeners react to the TabOpen event before removing the tab.
-  executeSoon(function() {
-    is(gBrowser.browsers[1].currentURI.spec, "about:blank",
-       "we will be removing an about:blank tab");
+  is(tab.linkedBrowser.currentURI.spec, "about:blank",
+     "we will be removing an about:blank tab");
 
-    gBrowser.removeTab(tab);
-
-    is(ss.getClosedTabCount(window), 0, "should still be no closed tabs");
+  let r = `rand-${Math.random()}`;
+  ss.setTabValue(tab, "foobar", r);
 
-    executeSoon(finish);
-  });
-}
+  yield promiseRemoveTab(tab);
+  let closedTabData = ss.getClosedTabData(window);
+  ok(!closedTabData.contains(r), "tab not stored in _closedTabs");
+});
--- a/browser/components/sessionstore/test/browser_628270.js
+++ b/browser/components/sessionstore/test/browser_628270.js
@@ -25,25 +25,26 @@ function test() {
   whenTabIsLoaded(tab, function () {
     // hide the newly created tab
     assertNumberOfVisibleTabs(2, "there are two visible tabs");
     gBrowser.showOnlyTheseTabs([gBrowser.tabs[0]]);
     assertNumberOfVisibleTabs(1, "there is one visible tab");
     ok(tab.hidden, "newly created tab is now hidden");
 
     // close and restore hidden tab
-    gBrowser.removeTab(tab);
-    tab = ss.undoCloseTab(window, 0);
+    promiseRemoveTab(tab).then(() => {
+      tab = ss.undoCloseTab(window, 0);
 
-    // check that everything was restored correctly, clean up and finish
-    whenTabIsLoaded(tab, function () {
-      is(tab.linkedBrowser.currentURI.spec, "about:mozilla", "restored tab has correct url");
+      // check that everything was restored correctly, clean up and finish
+      whenTabIsLoaded(tab, function () {
+        is(tab.linkedBrowser.currentURI.spec, "about:mozilla", "restored tab has correct url");
 
-      gBrowser.removeTab(tab);
-      finish();
+        gBrowser.removeTab(tab);
+        finish();
+      });
     });
   });
 }
 
 function whenTabIsLoaded(tab, callback) {
   tab.linkedBrowser.addEventListener("load", function onLoad() {
     tab.linkedBrowser.removeEventListener("load", onLoad, true);
     callback();
--- a/browser/components/sessionstore/test/browser_911547.js
+++ b/browser/components/sessionstore/test/browser_911547.js
@@ -24,17 +24,17 @@ add_task(function* test() {
   // origin document) and navigate to the data URI in the link.
   browser.contentDocument.getElementById("test_data_link").click();
   yield promiseBrowserLoaded(browser);
 
   is(browser.contentDocument.getElementById("test_id2").value, "ok",
      "CSP should block the script loaded by the clicked data URI");
 
   // close the tab
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   // open new tab and recover the state
   tab = ss.undoCloseTab(window, 0);
   yield promiseTabRestored(tab);
   browser = tab.linkedBrowser;
 
   is(browser.contentDocument.getElementById("test_id2").value, "ok",
      "CSP should block the script loaded by the clicked data URI after restore");
--- a/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js
+++ b/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js
@@ -1,25 +1,21 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
 
 // Tests that an about:privatebrowsing tab with no history will not
 // be saved into session store and thus, it will not show up in
 // Recently Closed Tabs.
 
 add_task(function* () {
-  while (ss.getClosedTabCount(window)) {
-    ss.forgetClosedTab(window, 0);
-  }
-
-  is(ss.getClosedTabCount(window), 0, "should be no closed tabs");
-
   let tab = gBrowser.addTab("about:privatebrowsing");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   is(gBrowser.browsers[1].currentURI.spec, "about:privatebrowsing",
      "we will be removing an about:privatebrowsing tab");
 
-  gBrowser.removeTab(tab);
-  is(ss.getClosedTabCount(window), 0, "should still be no closed tabs");
+  let r = `rand-${Math.random()}`;
+  ss.setTabValue(tab, "foobar", r);
+
+  yield promiseRemoveTab(tab);
+  let closedTabData = ss.getClosedTabData(window);
+  ok(!closedTabData.contains(r), "tab not stored in _closedTabs");
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_remove_tab.js
@@ -0,0 +1,242 @@
+"use strict";
+
+function* createTabWithRandomValue(url) {
+  let tab = gBrowser.addTab(url);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
+
+  // Set a random value.
+  let r = `rand-${Math.random()}`;
+  ss.setTabValue(tab, "foobar", r);
+
+  // Flush to ensure there are no scheduled messages.
+  TabState.flush(browser);
+
+  return {tab, r};
+}
+
+function isValueInClosedData(rval) {
+  return ss.getClosedTabData(window).includes(rval);
+}
+
+function restoreClosedTabWithValue(rval) {
+  let closedTabData = JSON.parse(ss.getClosedTabData(window));
+  let index = closedTabData.findIndex(function (data) {
+    return (data.state.extData && data.state.extData.foobar) == rval;
+  });
+
+  if (index == -1) {
+    throw new Error("no closed tab found for given rval");
+  }
+
+  return ss.undoCloseTab(window, index);
+}
+
+function promiseNewLocationAndHistoryEntryReplaced(browser, snippet) {
+  return ContentTask.spawn(browser, snippet, function* (snippet) {
+    let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
+    let shistory = webNavigation.sessionHistory;
+
+    // Evaluate the snippet that the changes the location.
+    eval(snippet);
+
+    return new Promise(resolve => {
+      let listener = {
+        OnHistoryReplaceEntry() {
+          shistory.removeSHistoryListener(this);
+          resolve();
+        },
+
+        QueryInterface: XPCOMUtils.generateQI([
+          Ci.nsISHistoryListener,
+          Ci.nsISupportsWeakReference
+        ])
+      };
+
+      shistory.addSHistoryListener(listener);
+
+      /* Keep the weak shistory listener alive. */
+      addEventListener("unload", function () {
+        try {
+          shistory.removeSHistoryListener(listener);
+        } catch (e) { /* Will most likely fail. */ }
+      });
+    });
+  });
+}
+
+function promiseHistoryEntryReplacedNonRemote(browser) {
+  let {listeners} = promiseHistoryEntryReplacedNonRemote;
+
+  return new Promise(resolve => {
+    let shistory = browser.webNavigation.sessionHistory;
+
+    let listener = {
+      OnHistoryReplaceEntry() {
+        shistory.removeSHistoryListener(this);
+        resolve();
+      },
+
+      QueryInterface: XPCOMUtils.generateQI([
+        Ci.nsISHistoryListener,
+        Ci.nsISupportsWeakReference
+      ])
+    };
+
+    shistory.addSHistoryListener(listener);
+    listeners.set(browser, listener);
+  });
+}
+promiseHistoryEntryReplacedNonRemote.listeners = new WeakMap();
+
+add_task(function* dont_save_empty_tabs() {
+  let {tab, r} = yield createTabWithRandomValue("about:blank");
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // No tab state worth saving.
+  ok(!isValueInClosedData(r), "closed tab not saved");
+  yield promise;
+
+  // Still no tab state worth saving.
+  ok(!isValueInClosedData(r), "closed tab not saved");
+});
+
+add_task(function* save_worthy_tabs_remote() {
+  let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+  ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // Tab state deemed worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+  yield promise;
+
+  // Tab state still deemed worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* save_worthy_tabs_nonremote() {
+  let {tab, r} = yield createTabWithRandomValue("about:robots");
+  ok(!tab.linkedBrowser.isRemoteBrowser, "browser is not remote");
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // Tab state deemed worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+  yield promise;
+
+  // Tab state still deemed worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* save_worthy_tabs_remote_final() {
+  let {tab, r} = yield createTabWithRandomValue("about:blank");
+  let browser = tab.linkedBrowser;
+  ok(browser.isRemoteBrowser, "browser is remote");
+
+  // Replace about:blank with a new remote page.
+  let snippet = 'webNavigation.loadURI("https://example.com/", null, null, null, null)';
+  yield promiseNewLocationAndHistoryEntryReplaced(browser, snippet);
+
+  // Remotness shouldn't have changed.
+  ok(browser.isRemoteBrowser, "browser is still remote");
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // No tab state worth saving (that we know about yet).
+  ok(!isValueInClosedData(r), "closed tab not saved");
+  yield promise;
+
+  // Turns out there is a tab state worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* save_worthy_tabs_nonremote_final() {
+  let {tab, r} = yield createTabWithRandomValue("about:blank");
+  let browser = tab.linkedBrowser;
+  ok(browser.isRemoteBrowser, "browser is remote");
+
+  // Replace about:blank with a non-remote entry.
+  browser.loadURI("about:robots");
+  ok(!browser.isRemoteBrowser, "browser is not remote anymore");
+
+  // Wait until the new entry replaces about:blank.
+  yield promiseHistoryEntryReplacedNonRemote(browser);
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // No tab state worth saving (that we know about yet).
+  ok(!isValueInClosedData(r), "closed tab not saved");
+  yield promise;
+
+  // Turns out there is a tab state worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(function* dont_save_empty_tabs_final() {
+  let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+  let browser = tab.linkedBrowser;
+
+  // Replace the current page with an about:blank entry.
+  let snippet = 'content.location.replace("about:blank")';
+  yield promiseNewLocationAndHistoryEntryReplaced(browser, snippet);
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // Tab state deemed worth saving (yet).
+  ok(isValueInClosedData(r), "closed tab saved");
+  yield promise;
+
+  // Turns out we don't want to save the tab state.
+  ok(!isValueInClosedData(r), "closed tab not saved");
+});
+
+add_task(function* undo_worthy_tabs() {
+  let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+  ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // Tab state deemed worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+
+  // Restore the closed tab before receiving its final message.
+  tab = restoreClosedTabWithValue(r);
+
+  // Wait for the final update message.
+  yield promise;
+
+  // Check we didn't add the tab back to the closed list.
+  ok(!isValueInClosedData(r), "tab no longer closed");
+
+  // Cleanup.
+  yield promiseRemoveTab(tab);
+});
+
+add_task(function* forget_worthy_tabs_remote() {
+  let {tab, r} = yield createTabWithRandomValue("https://example.com/");
+  ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+  // Remove the tab before the update arrives.
+  let promise = promiseRemoveTab(tab);
+
+  // Tab state deemed worth saving.
+  ok(isValueInClosedData(r), "closed tab saved");
+
+  // Forget the closed tab.
+  ss.forgetClosedTab(window, 0);
+
+  // Wait for the final update message.
+  yield promise;
+
+  // Check we didn't add the tab back to the closed list.
+  ok(!isValueInClosedData(r), "we forgot about the tab");
+});
--- a/browser/components/sessionstore/test/browser_broadcast.js
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -9,17 +9,17 @@ const INITIAL_VALUE = "browser_broadcast
  * This test ensures we won't lose tab data queued in the content script when
  * closing a tab.
  */
 add_task(function flush_on_tabclose() {
   let tab = yield createTabWithStorageData(["http://example.com"]);
   let browser = tab.linkedBrowser;
 
   yield modifySessionStorage(browser, {test: "on-tab-close"});
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://example.com"].test, "on-tab-close",
     "sessionStorage data has been flushed on TabClose");
 });
 
 /**
  * This test ensures we won't lose tab data queued in the content script when
@@ -54,17 +54,17 @@ add_task(function flush_on_duplicate() {
 
   yield modifySessionStorage(browser, {test: "on-duplicate"});
   let tab2 = ss.duplicateTab(window, tab);
   let {storage} = JSON.parse(ss.getTabState(tab2));
   is(storage["http://example.com"].test, "on-duplicate",
     "sessionStorage data has been flushed when duplicating tabs");
 
   yield promiseTabRestored(tab2);
-  gBrowser.removeTab(tab2);
+  yield promiseRemoveTab(tab2);
   [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://example.com"].test, "on-duplicate",
     "sessionStorage data has been flushed when duplicating tabs");
 
   gBrowser.removeTab(tab);
 });
 
 /**
@@ -123,17 +123,17 @@ add_task(function flush_on_tabclose_racy
   // Flush to make sure we start with an empty queue.
   TabState.flush(browser);
 
   yield modifySessionStorage(browser, {test: "on-tab-close-racy"});
 
   // Flush all data contained in the content script but send it using
   // asynchronous messages.
   TabState.flushAsync(browser);
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://example.com"].test, "on-tab-close-racy",
     "sessionStorage data has been merged correctly to prevent data loss");
 });
 
 function promiseNewWindow() {
   let deferred = Promise.defer();
--- a/browser/components/sessionstore/test/browser_cleaner.js
+++ b/browser/components/sessionstore/test/browser_cleaner.js
@@ -66,18 +66,18 @@ add_task(function* test_open_and_close()
   is(state.windows[0].tabs[1].closedAt || false, false, "1. Second tab doesn't have closedAt");
 
 
 
   info("2. Making sure that after closing, we have closedAt");
 
   // Now close stuff, this should add closeAt
   yield promiseWindowClosed(newWin);
-  gBrowser.removeTab(newTab1);
-  gBrowser.removeTab(newTab2);
+  yield promiseRemoveTab(newTab1);
+  yield promiseRemoveTab(newTab2);
 
   state = CLOSED_STATE = JSON.parse(ss.getBrowserState());
 
   is(state.windows[0].closedAt || false, false, "2. Main window doesn't have closedAt");
   ok(isRecent(state._closedWindows[0].closedAt), "2. Second window was closed recently");
   ok(isRecent(state.windows[0]._closedTabs[0].closedAt), "2. First tab was closed recently");
   ok(isRecent(state.windows[0]._closedTabs[1].closedAt), "2. Second tab was closed recently");
 });
--- a/browser/components/sessionstore/test/browser_dying_cache.js
+++ b/browser/components/sessionstore/test/browser_dying_cache.js
@@ -15,17 +15,17 @@ add_task(function* test() {
   let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
   win.gBrowser.selectedBrowser.loadURIWithFlags("about:robots", flags);
   yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
 
   // Open a second tab and close the first one.
   let tab = win.gBrowser.addTab("about:mozilla");
   yield promiseBrowserLoaded(tab.linkedBrowser);
   TabState.flush(tab.linkedBrowser);
-  win.gBrowser.removeTab(win.gBrowser.tabs[0]);
+  yield promiseRemoveTab(win.gBrowser.tabs[0]);
 
   // Make sure our window is still tracked by sessionstore
   // and the window state is as expected.
   ok("__SSi" in win, "window is being tracked by sessionstore");
   ss.setWindowValue(win, "foo", "bar");
   checkWindowState(win);
 
   let state = ss.getWindowState(win);
--- a/browser/components/sessionstore/test/browser_formdata.js
+++ b/browser/components/sessionstore/test/browser_formdata.js
@@ -25,17 +25,17 @@ add_task(function test_formdata() {
       let browser = tab.linkedBrowser;
       yield promiseBrowserLoaded(browser);
 
       // Modify form data.
       yield setInputValue(browser, {id: "txt", value: OUTER_VALUE});
       yield setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
 
       // Remove the tab.
-      gBrowser.removeTab(tab);
+      yield promiseRemoveTab(tab);
     });
   }
 
   yield createAndRemoveTab();
   let [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
   is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
   is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
 
@@ -114,17 +114,17 @@ add_task(function test_nested() {
   let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Modify the input field's value.
   yield sendMessage(browser, "ss-test:sendKeyEvent", {key: "m", frame: 0});
 
   // Remove the tab and check that we stored form data correctly.
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   let [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
   is(JSON.stringify(formdata), JSON.stringify(FORM_DATA),
     "formdata for iframe stored correctly");
 
   // Restore the closed tab.
   tab = ss.undoCloseTab(window, 0);
   browser = tab.linkedBrowser;
   yield promiseTabRestored(tab);
@@ -151,28 +151,28 @@ add_task(function test_design_mode() {
   let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Modify the document content.
   yield sendMessage(browser, "ss-test:sendKeyEvent", {key: "m"});
 
   // Close and restore the tab.
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   tab = ss.undoCloseTab(window, 0);
   browser = tab.linkedBrowser;
   yield promiseTabRestored(tab);
 
   // Check that the innerHTML value was restored.
   let html = yield getInnerHTML(browser);
   let expected = "<h1>Mmozilla</h1><script>document.designMode='on'</script>";
   is(html, expected, "editable document has been restored correctly");
 
   // Close and restore the tab.
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   tab = ss.undoCloseTab(window, 0);
   browser = tab.linkedBrowser;
   yield promiseTabRestored(tab);
 
   // Check that the innerHTML value was restored.
   html = yield getInnerHTML(browser);
   expected = "<h1>Mmozilla</h1><script>document.designMode='on'</script>";
   is(html, expected, "editable document has been restored correctly");
@@ -227,17 +227,17 @@ add_task(function test_ccNumbers() {
     let tab = gBrowser.addTab(URL);
     let browser = tab.linkedBrowser;
     yield promiseBrowserLoaded(browser);
 
     // Set form value.
     yield setInputValue(browser, {id: "txt", value: formValue});
 
     // Remove the tab.
-    gBrowser.removeTab(tab);
+    yield promiseRemoveTab(tab);
   }
 
   // Test that valid CC numbers are not collected.
   for (let number of validCCNumbers) {
     yield createAndRemoveTab(number);
     let [{state}] = JSON.parse(ss.getClosedTabData(window));
     ok(!("formdata" in state), "valid CC numbers are not collected");
   }
--- a/browser/components/sessionstore/test/browser_frame_history.js
+++ b/browser/components/sessionstore/test/browser_frame_history.js
@@ -27,17 +27,17 @@ add_task(function() {
   yield promise;
 
   info("Clicking on link 2, 1 load should take place");
   promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
   EventUtils.sendMouseEvent({type:"click"}, links[1], browser_b.contentWindow);
   yield promise;
 
   info("Close then un-close page, 4 loads should take place");
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   let newTab = ss.undoCloseTab(window, 0);
   yield waitForLoadsInBrowser(newTab.linkedBrowser, 4);
 
   info("Go back in time, 1 load should take place");
   gBrowser.goBack();
   yield waitForLoadsInBrowser(newTab.linkedBrowser, 1);
 
   let expectedURLEnds = ["a.html", "b.html", "c1.html"];
@@ -72,17 +72,17 @@ add_task(function() {
   yield promise;
 
   info("iframe: Clicking on link 2, 1 load should take place");
   promise = waitForLoadsInBrowser(tab.linkedBrowser, 1);
   EventUtils.sendMouseEvent({type:"click"}, links[1], browser_b.contentWindow);
   yield promise;
 
   info("iframe: Close then un-close page, 5 loads should take place");
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
   let newTab = ss.undoCloseTab(window, 0);
   yield waitForLoadsInBrowser(newTab.linkedBrowser, 5);
 
   info("iframe: Go back in time, 1 load should take place");
   gBrowser.goBack();
   yield waitForLoadsInBrowser(newTab.linkedBrowser, 1);
 
   let expectedURLEnds = ["a.html", "b.html", "c1.html"];
--- a/browser/components/sessionstore/test/browser_pageStyle.js
+++ b/browser/components/sessionstore/test/browser_pageStyle.js
@@ -54,17 +54,17 @@ add_task(function page_style() {
  * received and the page style is persisted correctly.
  */
 add_task(function nested_page_style() {
   let tab = gBrowser.addTab(URL_NESTED);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   yield enableSubDocumentStyleSheetsForSet(browser, "alternate");
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   let [{state: {pageStyle}}] = JSON.parse(ss.getClosedTabData(window));
   let expected = JSON.stringify({children: [{pageStyle: "alternate"}]});
   is(JSON.stringify(pageStyle), expected, "correct pageStyle persisted");
 });
 
 function getStyleSheets(browser) {
   return sendMessage(browser, "ss-test:getStyleSheets");
--- a/browser/components/sessionstore/test/browser_sessionHistory.js
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -10,17 +10,17 @@ add_task(function test_load_start() {
   // Create a new tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Load a new URI but remove the tab before it has finished loading.
   browser.loadURI("about:mozilla");
   yield promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   // Undo close the tab.
   tab = ss.undoCloseTab(window, 0);
   browser = tab.linkedBrowser;
   yield promiseTabRestored(tab);
 
   // Check that the correct URL was restored.
   is(browser.currentURI.spec, "about:mozilla", "url is correct");
--- a/browser/components/sessionstore/test/browser_sessionStorage.js
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -124,54 +124,54 @@ add_task(function purge_domain() {
 
 /**
  * This test ensures that collecting sessionStorage data respects the privacy
  * levels as set by the user.
  */
 add_task(function respect_privacy_level() {
   let tab = gBrowser.addTab(URL + "&secure");
   yield promiseBrowserLoaded(tab.linkedBrowser);
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
     "http sessionStorage data has been saved");
   is(storage["https://example.com"].test, INNER_VALUE,
     "https sessionStorage data has been saved");
 
   // Disable saving data for encrypted sites.
   Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
 
   tab = gBrowser.addTab(URL + "&secure");
   yield promiseBrowserLoaded(tab.linkedBrowser);
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
     "http sessionStorage data has been saved");
   ok(!storage["https://example.com"],
     "https sessionStorage data has *not* been saved");
 
   // Disable saving data for any site.
   Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
 
   // Check that duplicating a tab copies all private data.
   tab = gBrowser.addTab(URL + "&secure");
   yield promiseBrowserLoaded(tab.linkedBrowser);
   let tab2 = gBrowser.duplicateTab(tab);
   yield promiseTabRestored(tab2);
-  gBrowser.removeTab(tab);
+  yield promiseRemoveTab(tab);
 
   // With privacy_level=2 the |tab| shouldn't have any sessionStorage data.
   [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   ok(!storage, "sessionStorage data has *not* been saved");
 
   // Restore the default privacy level and close the duplicated tab.
   Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
-  gBrowser.removeTab(tab2);
+  yield promiseRemoveTab(tab2);
 
   // With privacy_level=0 the duplicated |tab2| should persist all data.
   [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
     "http sessionStorage data has been saved");
   is(storage["https://example.com"].test, INNER_VALUE,
     "https sessionStorage data has been saved");
 });
--- a/browser/components/sessionstore/test/browser_telemetry.js
+++ b/browser/components/sessionstore/test/browser_telemetry.js
@@ -71,49 +71,49 @@ add_task(function history() {
     gt(statistics2[KEY], statistics[KEY], "The total size of HISTORY has increased");
 
 // Almost nothing else should
     for (let k of ["FORMDATA", "DOM_STORAGE", "CLOSED_WINDOWS", "CLOSED_TABS_IN_OPEN_WINDOWS"]) {
       is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased");
     }
   } finally {
     if (tab) {
-      gBrowser.removeTab(tab);
+      yield promiseRemoveTab(tab);
     }
   }
 });
 
 /**
  * Test CLOSED_TABS_IN_OPEN_WINDOWS key.
  */
 add_task(function close_tab() {
   let KEY = Keys.CLOSED_TABS_IN_OPEN_WINDOWS;
   let tab = gBrowser.addTab("http://example.org:80/?close_tab");
   yield promiseBrowserLoaded(tab.linkedBrowser);
   try {
     TabState.flush(tab.linkedBrowser);
     let statistics = yield promiseStats();
 
     info("Now closing a tab");
-    gBrowser.removeTab(tab);
+    yield promiseRemoveTab(tab);
     tab = null;
     let statistics2 = yield promiseStats();
 
     isnot(statistics[KEY], undefined, "Key was defined");
     isnot(statistics2[KEY], undefined, "Key is still defined");
     gt(statistics2[KEY], statistics[KEY], "The total size of CLOSED_TABS_IN_OPEN_WINDOWS has increased");
 
     // Almost nothing else should change
     for (let k of ["FORMDATA", "DOM_STORAGE", "CLOSED_WINDOWS"]) {
       is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased");
     }
 
   } finally {
     if (tab) {
-      gBrowser.removeTab(tab);
+      yield promiseRemoveTab(tab);
     }
   }
 });
 
 /**
  * Test OPEN_WINDOWS key.
  */
 add_task(function open_window() {
@@ -197,17 +197,17 @@ add_task(function dom_storage() {
 
     // Almost nothing else should change
     for (let k of ["CLOSED_TABS_IN_OPEN_WINDOWS", "FORMDATA", "CLOSED_WINDOWS"]) {
       is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased");
     }
 
   } finally {
     if (tab) {
-      gBrowser.removeTab(tab);
+      yield promiseRemoveTab(tab);
     }
   }
 });
 
 /**
  * Test FORMDATA key.
  */
 add_task(function formdata() {
@@ -230,17 +230,17 @@ add_task(function formdata() {
     gt(statistics2[KEY], statistics[KEY], "The total size of FORMDATA has increased");
 
     // Almost nothing else should
     for (let k of ["DOM_STORAGE", "CLOSED_WINDOWS", "CLOSED_TABS_IN_OPEN_WINDOWS"]) {
       is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased");
     }
   } finally {
     if (tab) {
-      gBrowser.removeTab(tab);
+      yield promiseRemoveTab(tab);
     }
   }
 });
 
 add_task(function* test_sessionRestoreInit() {
    let info = Cc["@mozilla.org/toolkit/app-startup;1"].
      getService(Ci.nsIAppStartup).
      getStartupInfo();
--- a/browser/components/sessionstore/test/content.js
+++ b/browser/components/sessionstore/test/content.js
@@ -60,18 +60,20 @@ let historyListener = {
   },
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsISHistoryListener,
     Ci.nsISupportsWeakReference
   ])
 };
 
-docShell.QueryInterface(Ci.nsIWebNavigation).
+let {sessionHistory} = docShell.QueryInterface(Ci.nsIWebNavigation);
+if (sessionHistory) {
   sessionHistory.addSHistoryListener(historyListener);
+}
 
 /**
  * This frame script is only loaded for sessionstore mochitests. It enables us
  * to modify and query docShell data when running with multiple processes.
  */
 
 addEventListener("hashchange", function () {
   sendAsyncMessage("ss-test:hashchange");
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -522,8 +522,12 @@ const FORM_HELPERS = [
   "getMultipleSelected", "setMultipleSelected",
   "getFileNameArray", "setFileNameArray",
 ];
 
 for (let name of FORM_HELPERS) {
   let msg = "ss-test:" + name;
   this[name] = (browser, data) => sendMessage(browser, msg, data);
 }
+
+function promiseRemoveTab(tab) {
+  return BrowserTestUtils.removeTab(tab);
+}
--- a/browser/components/tabview/content.js
+++ b/browser/components/tabview/content.js
@@ -56,27 +56,29 @@ addEventListener("MozAfterPaint", Window
 // WindowMessageHandler
 //
 // Handles messages sent by the chrome process.
 let WindowMessageHandler = {
   // ----------
   // Function: isDocumentLoaded
   // Checks if the currently active document is loaded.
   isDocumentLoaded: function WMH_isDocumentLoaded(cx) {
-    let isLoaded = (content.document.readyState != "uninitialized" &&
+    let isLoaded = (content &&
+                    content.document.readyState != "uninitialized" &&
                     !webProgress.isLoadingDocument);
 
     sendAsyncMessage(cx.name, {isLoaded: isLoaded});
   },
 
   // ----------
   // Function: isImageDocument
   // Checks if the currently active document is an image document or not.
   isImageDocument: function WMH_isImageDocument(cx) {
-    let isImageDocument = (content.document instanceof Ci.nsIImageDocument);
+    let isImageDocument = (content &&
+                           content.document instanceof Ci.nsIImageDocument);
 
     sendAsyncMessage(cx.name, {isImageDocument: isImageDocument});
   },
 
   waitForDocumentLoad: function WMH_waitForDocumentLoad() {
     addEventListener("load", function listener() {
       removeEventListener("load", listener, true);
       sendAsyncMessage("Panorama:documentLoaded");
--- a/browser/components/tabview/favicons.js
+++ b/browser/components/tabview/favicons.js
@@ -84,17 +84,19 @@ let FavIcons = {
 
     // If the tab image's url starts with http(s), fetch icon from favicon
     // service via the moz-anno protocol.
     if (/^https?:/.test(tabImage)) {
       let tabImageURI = gWindow.makeURI(tabImage);
       tabImage = this._favIconService.getFaviconLinkForIcon(tabImageURI).spec;
     }
 
-    tabImage = PlacesUtils.getImageURLForResolution(window, tabImage);
+    if (tabImage) {
+      tabImage = PlacesUtils.getImageURLForResolution(window, tabImage);
+    }
 
     callback(tabImage);
   },
 
   // ----------
   // Function: _getFavIconForHttpDocument
   // Retrieves the favicon for tab containg a http(s) document.
   _getFavIconForHttpDocument:
--- a/browser/components/tabview/test/browser_tabview_bug608037.js
+++ b/browser/components/tabview/test/browser_tabview_bug608037.js
@@ -19,24 +19,25 @@ function test() {
 }
 
 function onTabViewWindowLoaded() {
   let contentWindow = TabView.getContentWindow();
   let groupItems = contentWindow.GroupItems.groupItems;
   is(groupItems.length, 1, "There is only one group");
   is(groupItems[0].getChildren().length, 3, "The group has three tab items");
 
-  gBrowser.removeTab(tabTwo);
-  ok(TabView.isVisible(), "Tab View is still visible after removing a tab");
-  is(groupItems[0].getChildren().length, 2, "The group has two tab items");
+  BrowserTestUtils.removeTab(tabTwo).then(() => {
+    ok(TabView.isVisible(), "Tab View is still visible after removing a tab");
+    is(groupItems[0].getChildren().length, 2, "The group has two tab items");
 
-  restoreTab(function (tabTwo) {
-    ok(TabView.isVisible(), "Tab View is still visible after restoring a tab");
-    is(groupItems[0].getChildren().length, 3, "The group still has three tab items");
+    restoreTab(function (tabTwo) {
+      ok(TabView.isVisible(), "Tab View is still visible after restoring a tab");
+      is(groupItems[0].getChildren().length, 3, "The group still has three tab items");
 
-    // clean up and finish
-    hideTabView(function () {
-      gBrowser.removeTab(tabOne);
-      gBrowser.removeTab(tabTwo);
-      finish();
+      // clean up and finish
+      hideTabView(function () {
+        gBrowser.removeTab(tabOne);
+        gBrowser.removeTab(tabTwo);
+        finish();
+      });
     });
   });
 }
--- a/browser/components/tabview/test/browser_tabview_bug624847.js
+++ b/browser/components/tabview/test/browser_tabview_bug624847.js
@@ -51,28 +51,29 @@ function test() {
   }
 
   let testUndoCloseWithSelectedBlankTab = function () {
     prefix = 'unpinned';
     let tab = createTab();
     assertNumberOfTabs(2);
 
     afterAllTabsLoaded(function () {
-      win.gBrowser.removeTab(tab);
-      assertNumberOfTabs(1);
-      assertNumberOfPinnedTabs(0);
+      BrowserTestUtils.removeTab(tab).then(() => {
+        assertNumberOfTabs(1);
+        assertNumberOfPinnedTabs(0);
 
-      restoreTab(function () {
-        prefix = 'unpinned-restored';
-        assertValidPrerequisites();
-        assertGroupItemPreserved();
+        restoreTab(function () {
+          prefix = 'unpinned-restored';
+          assertValidPrerequisites();
+          assertGroupItemPreserved();
 
-        createBlankTab();
-        afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab, win);
-      }, 0, win);
+          createBlankTab();
+          afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab, win);
+        }, 0, win);
+      });
     }, win);
   }
 
   let testUndoCloseWithSelectedBlankPinnedTab = function () {
     prefix = 'pinned';
     assertNumberOfTabs(2);
 
     afterAllTabsLoaded(function () {
--- a/browser/components/tabview/test/browser_tabview_bug628270.js
+++ b/browser/components/tabview/test/browser_tabview_bug628270.js
@@ -65,27 +65,34 @@ function test() {
 
   let testRestoreTabFromInactiveGroup = function () {
     prefix = 'restore';
     activateFirstGroupItem();
 
     let groupItem = getGroupItem(1);
     let tabItem = groupItem.getChild(0);
 
+    // Wait until the tab has been removed but close it ourselves.
+    let promise = BrowserTestUtils.removeTab(tabItem.tab, {dontRemove: true});
+
+    // Close the tab.
     EventUtils.synthesizeMouseAtCenter(
       tabItem.$close[0], {}, TabView.getContentWindow());
-    assertNumberOfTabsInGroup(groupItem, 1);
 
-    restoreTab(function () {
-      assertNumberOfTabsInGroup(groupItem, 2);
+    promise.then(() => {
+      assertNumberOfTabsInGroup(groupItem, 1);
 
-      activateFirstGroupItem();
-      gBrowser.removeTab(gBrowser.tabs[1]);
-      gBrowser.removeTab(gBrowser.tabs[1]);
-      hideTabView(finishTest);
+      restoreTab(function () {
+        assertNumberOfTabsInGroup(groupItem, 2);
+
+        activateFirstGroupItem();
+        gBrowser.removeTab(gBrowser.tabs[1]);
+        gBrowser.removeTab(gBrowser.tabs[1]);
+        hideTabView(finishTest);
+      });
     });
   }
 
   waitForExplicitFinish();
   assertTabViewIsHidden();
   registerCleanupFunction(function () TabView.hide());
 
   showTabView(function () {
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -656,17 +656,17 @@ this.BrowserUITelemetry = {
     "media-playbackrate-050x", "media-playbackrate-100x",
     "media-playbackrate-150x", "media-playbackrate-200x",
     "media-showcontrols", "media-hidecontrols", "video-showstats",
     "video-hidestats", "video-fullscreen", "leave-dom-fullscreen",
     "reloadimage", "viewimage", "viewvideo", "copyimage-contents", "copyimage",
     "copyvideourl", "copyaudiourl", "saveimage", "shareimage", "sendimage",
     "setDesktopBackground", "viewimageinfo", "viewimagedesc", "savevideo",
     "sharevideo", "saveaudio", "video-saveimage", "sendvideo", "sendaudio",
-    "ctp-play", "ctp-hide", "sharepage", "savepage", "markpageMenu",
+    "ctp-play", "ctp-hide", "sharepage", "savepage", "pocket", "markpageMenu",
     "viewbgimage", "undo", "cut", "copy", "paste", "delete", "selectall",
     "keywordfield", "searchselect", "shareselect", "frame", "showonlythisframe",
     "openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe",
     "printframe", "viewframesource", "viewframeinfo",
     "viewpartialsource-selection", "viewpartialsource-mathml",
     "viewsource", "viewinfo", "spell-check-enabled",
     "spell-add-dictionaries-main", "spell-dictionaries",
     "spell-dictionaries-menu", "spell-add-dictionaries",
--- a/browser/modules/CustomizationTabPreloader.jsm
+++ b/browser/modules/CustomizationTabPreloader.jsm
@@ -150,16 +150,17 @@ HiddenBrowser.prototype = {
   _createBrowser: function () {
     if (!this._hiddenFrame) {
       this._hiddenFrame = new HiddenFrame();
     }
 
     this._hiddenFrame.get().then(aFrame => {
       let doc = aFrame.document;
       this._browser = doc.createElementNS(XUL_NS, "browser");
+      this._browser.permanentKey = {};
       this._browser.setAttribute("type", "content");
       this._browser.setAttribute("src", CUSTOMIZATION_URL);
       this._browser.style.width = "400px";
       this._browser.style.height = "400px";
       doc.getElementById("win").appendChild(this._browser);
     });
   },
 
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -518,16 +518,22 @@ menuitem:not([type]):not(.menuitem-toolt
   list-style-image: url("chrome://browser/skin/places/unsortedBookmarks.png");
 }
 
 #menu_readingList,
 #BMB_readingList {
   list-style-image: url("chrome://browser/skin/readinglist/readinglist-icon.svg");
 }
 
+#panelMenu_pocket,
+#menu_pocket,
+#BMB_pocket {
+  list-style-image: url("chrome://browser/content/pocket/panels/img/pocketmenuitem16.png");
+}
+
 #menu_openDownloads {
   list-style-image: url("chrome://browser/skin/Toolbar-small.png");
   -moz-image-region: rect(0px 16px 16px 0px);
 }
 
 #menu_openAddons {
   list-style-image: url("chrome://mozapps/skin/extensions/extensionGeneric-16.png");
 }
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -563,16 +563,34 @@ toolbarpaletteitem[place="palette"] > #p
   }
 }
 
 /* #menu_readingList, svg icons don't work in the mac native menubar */
 #BMB_readingList {
   list-style-image: url("chrome://browser/skin/readinglist/readinglist-icon.svg");
 }
 
+#panelMenu_pocket,
+#menu_pocket,
+#BMB_pocket {
+  list-style-image: url("chrome://browser/content/pocket/panels/img/pocketmenuitem16.png");
+}
+
+@media (min-resolution: 2dppx) {
+  #panelMenu_pocket,
+  #menu_pocket,
+  #BMB_pocket {
+    list-style-image: url("chrome://browser/content/pocket/panels/img/pocketmenuitem16@2x.png");
+  }
+
+  #panelMenu_pocket > .toolbarbutton-icon {
+    width: 16px;
+  }
+}
+
 /* ----- PRIMARY TOOLBAR BUTTONS ----- */
 
 toolbar .toolbarbutton-1:not([type="menu-button"]),
 .toolbarbutton-1 > .toolbarbutton-menubutton-button,
 .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-box-orient: vertical;
   height: 24px;
   padding: 0;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2292,16 +2292,22 @@ notification[value="loop-sharing-notific
 }
 
 #menu_readingList,
 #BMB_readingList {
   list-style-image: url("chrome://browser/skin/readinglist/readinglist-icon.svg");
   -moz-image-region: auto;
 }
 
+#panelMenu_pocket,
+#menu_pocket,
+#BMB_pocket {
+  list-style-image: url("chrome://browser/content/pocket/panels/img/pocketmenuitem16.png");
+}
+
 /* ::::: Keyboard UI Panel ::::: */
 
 .KUI-panel {
   -moz-appearance: none;
   background: rgba(27%,27%,27%,.9) url(KUI-background.png) repeat-x;
   color: white;
   border-style: none;
   border-radius: 20px;
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -29,16 +29,17 @@
 #include "nsIDOMEventListener.h"
 #include "nsIDOMNodeList.h"
 #include "nsIDOMXULCommandDispatcher.h"
 #include "nsIDOMXULElement.h"
 #include "nsIDOMElementCSSInlineStyle.h"
 #include "nsIDOMXULSelectCntrlItemEl.h"
 #include "nsIDocument.h"
 #include "nsLayoutStylesheetCache.h"
+#include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/EventStates.h"
 #include "nsFocusManager.h"
 #include "nsHTMLStyleSheet.h"
 #include "nsIJSRuntimeService.h"
 #include "nsNameSpaceManager.h"
 #include "nsIObjectInputStream.h"
@@ -1596,16 +1597,20 @@ nsXULElement::LoadSrc()
     if (!slots->mFrameLoader) {
         // false as the last parameter so that xul:iframe/browser/editor
         // session history handling works like dynamic html:iframes.
         // Usually xul elements are used in chrome, which doesn't have
         // session history at all.
         slots->mFrameLoader = nsFrameLoader::Create(this, false);
         NS_ENSURE_TRUE(slots->mFrameLoader, NS_OK);
 
+        (new AsyncEventDispatcher(this,
+                                  NS_LITERAL_STRING("XULFrameLoaderCreated"),
+                                  /* aBubbles */ true))->RunDOMEventWhenSafe();
+
         if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::prerendered,
                         NS_LITERAL_STRING("true"), eIgnoreCase)) {
             nsresult rv = slots->mFrameLoader->SetIsPrerendered();
             NS_ENSURE_SUCCESS(rv,rv);
         }
     }
 
     return slots->mFrameLoader->LoadFrame();
index df814faba2b7c2970645db530f6257a2d96ab729..d6e11f8f8a63f2ae431dcb524c0d9e4cd51b3879
GIT binary patch
literal 681
zc$@*L0#^NrP)<h;3K|Lk000e1NJLTq001xm001Ni1^@s6&qcWk0007TNkl<Zc-qyM
z&#O&Q6vwyx3Q1w2WMIm~OerO?*E-jGJRC~c>zvC;^wQ&%BHWnE%s-%%+H2p79=ZcE
zcmutG;tdExQc97$=ht<&$sxMu*S+V*w^p6eI-j%7+IxMMD6{G<+@LhHiRA~<@<sRg
za%0+EAgUnW$bE|CH>G9guV9$;Tf<8$2p<M9JPK|PEiNTIF}NXouYmT=jyt!C@L?D{
zT^e{#<KEctVJl5n6F!Vjg~Kc3>`4wE-bj^&x8WPaA`k5DEbxl)LUH%Jh7r>8Yg%~H
z@_d!y&lcA<26ttGXFMa~sPUjNZ~XqDC1ry57{{aC-#vFZF<kG)XT|cT((sJf*;ahz
z;gpC{pyevX@bbS23|{JVI&)GDDV94k!5eu}MCk;NtgzLNsDe(~R=$SeYzG!cMU+M-
za3L`~j`u6&ldg?jGiZ3*zNF!J%>IOn36sB5E!@;IXA_5%hS}8cERV$>=M2x2zQn?L
z?0AEKR|bQmBmHCgaSm7hTq-<vc4UKxdKypiFd})>xwsp&-DO$eNy85^Kq)tWXR}R^
z=_53}$^!2*9u`?-6A;6UO_*=MchA7V{)~E!8%e|4JmIP791#7~5sj-A%c)!{-^Bf5
z5E`3w7ZJmaLBZQ4h8SEiXeq{X_G;@lyj>$jUb05aZvOp%x3<DAGa_%*xwgIw@H%79
z&)Q;$qSQ5TEA7Bb3vca(C|bc(FrOH&C0s;is0ofJldR3PhcQOae<5~2lu>@Gwnedg
z-wWP1xLuT4{+-%d#q#Mt@YdZL*ea?ZyJ8aKRmJe;_jSv1;^-~8E>DJEP_aXj*M5U0
P00000NkvXXu0mjfQy)$H
index fe4a03dabda5caf5cbd656404dee41172a80f3f8..b97f1262427d6275bb60c0507eb39b4282bfea88
GIT binary patch
literal 493
zc$@+40TTX+P)<h;3K|Lk000e1NJLTq001BW000;W1^@s6n^XTZ0005BNkl<Zc-qC5
zze@u#6vu<Ve-4hWqM)Pyfm_m^?c|MeX_W&D4ubACxVQ-J;vk4{HfigX;xDX-6bDB|
zK}RQ1P#i_;%VG-sajDH+zYuPDd7qc$o0mgj$iA_=ElxHm!*B3?O0dWr0)+|BSqzu|
z2FE`%Lue>)%1Ktj<)Dmw!`EmFTo!@vgM%}i5fGmO-rED0Uf2o3a9j;eO37S&SofJT
z6*w61N-NwcI$g?ePA*eLbEqoGLM}cIm2E?SOL>$81ix54o*%3Mk4y_kRt7`2+HlHF
zhcBGrv<e)?x$VUUs*+G|b^=!ZQpcs9JgaIr80kQ9<!3eFLKY*ZjT4aDWZoG2KOCQ|
zl+1oqPZ0E}a9>}cch6LuxvS{*BU+?=sF8##6`RHvBuuHoA-=F&i1Z>qEplTDw)t61
z(lwySyo4*rW_T3be60n}$f0v*_Mm1ZDu&6sEv`JP0{8R8ns}?5E`Sp*+A-20nk7+4
z_@Tb-I4y1m(kbf2CL#Hw!nV5-u31ZYW28}|0{FgLHak&}5`Y9JTQ#<QPr^fx_?3ng
jKZu@@;CNj=RITY3n)Cab<R#|}00000NkvXXu0mjfBtqHZ
index b9c6f1ad5ffee5f487e213700bb7e7126872a8bc..f34adda6bbd4a58db2fefc39b08cf34da7517665
GIT binary patch
literal 931
zc$@*F16=%xP)<h;3K|Lk000e1NJLTq002M$001xu1^@s6p-X%p000AONkl<Zc-rNf
zNoW*76oyMhQQY@kQBlMVQPhKq=)rn5lSBi~NwlkbCPPdR6})+gCqeY$Rm6h_Ay98-
zvY~hg;u6%DsM$~yjZ1LX;1U#d?59FQAd;S`ndzSS;n6UMX}*51>Md0)-&B>X8Y*M_
zfK>dNfzWF|yX@!bsNyqOzAKU_nJy8U48@cEO)CCQ#vzc`@CNcuf*si4n-vqHFyWm@
z=rd7l%eS!aAUGHH*+SUld-!rJo#w#1HF&<iL5%Y?ybaM#FsYhEI9agpWD<F+@8Hb_
zG1Y-j+dlaZ9(kJ{crs`ySa>p1@e_p#PuA^=ED)_No+1(MX@y7VHRFK|1qm+||L7Zd
zL&06G@W?+%1?vhD-avS7#DZ%*4_}F;lXb&WD{Lyh#uxAs`5nEU=Wk^!u-bi>rNkVG
z5RwWiOcYyWqa5i!cz6>@z$$kmrH;@DiE!Ko|Jg5$9$1Os)(pb8>J58%E#rZe%#99R
z4~;Suzx<zY`emRvq7y!RAv8%hyn*~xSq)$AZs%=|4mXfD46=<pPan&R@Dlk;GcK07
zpQIvGyKE$GmDd){P6wWv5zTA9N7fcEWo{*7a4OXohGRwRy#OzfzbM1N64&rXLTse9
zokouBGf?bigVesd;6#aVTPHk?w`6V6VwQ@ef_gn3fMc|jvJQ{%EE*GwSgMl9pK44!
zHCCczY?k23x~;n4LY9uGQqr7)+9EZ@3;M&`zc*{n^G}@jq#-Bu=`BC0di=Y=?wp_<
z9^rAgJ}{r@kfoqhE4;HgewRudq;#p-^N;LqaURntso*UAx#tbxp&8#Xm+4k@GBnJo
z1-xn0i#UZ>hGZ%pq0sf9=kMFuWfseZWSnoNPRc``@5=h+2HIhr-aLnfyi?W(X0Tl7
z@Wnl&C4$P`2Y_}u`ewOQGMOA=B0uV)SI|~W(P0SlicBSTNrW$1^88(z3Nx>X28&dC
zXA++F9U6~$`$mR>s~Pfq3(X0bm*osi`5Db?zL^>(<|qBD^M7b)Y;ID+#QZ2LCbrXg
zTN<9f5xo=|&-^SJMNzeug6Er@^Ld|(yy#}GK=*iQa+G(Ml<CUJAR3!16^)@WEMH}z
z*lQ^GI?&9}IZ(-Cg)z9oKsaYx4^@zVPJfVBQj{={={H&T8rnIhlPCZH002ovPDHLk
FV1mKoyD0zw
index 03ba871f61efbab70f5ffaf4662be3b96d989b0c..b4bb48d31927d21fd58a3347de8e2b5a63e2ed21
GIT binary patch
literal 1339
zc$_U$2~ZPP7+wx7f}lgi6GfD!95tw*MXf+M+-xoqBnYBrcY~=%t8wrEZ7JZvh~mJA
zgi#BFS_-t;O+chb+E5fk3KNDR2#6Am2y%p~fQ3f--pbC~xBvT(@B9Au&z={HP)GZv
z_6UMFV%)Gu@WsK)wX*?xU=<=k5UU}vASxUN)LfiGWfYfLD6k0)hO1Gz7L!3gN>Tz`
z!vh|W33A}hELd1BB(<O%BE)OCGMc}a;(-vU<;f}j-ZE4Ms>%frR?RGm8&C0Ja+xq?
zIt7P}7~E-yOlbIWt%#s!lmJ7~q#%Hw`bYu&<ZV8K29LxYOh#is8=;0oFiHc>7^&ss
zG!NI~N9v{VYKTv2ML3NS8gA*d6dd<a5*dxcDjG%!ikG5KAz<&Y5rT2fbjt9OKqNiW
z0@MjGX{Z_3V4&IQ>1l!j26{j*C25qYMHS^j8O@Vxd7ukI&6WQq1uQ@w%w&>*fos5}
zi2x=^aZ?^iaWy8V1)y9}4Md_0FPGFXs?_i{02hG$cohm7g5Zo(UcrM6G7}**5l{f5
z6Bys#!Dfc5M*p%CuLtwKc{EbA9eHIQ860@&J-lw{gYTNj*L@rQ``6<6`TKshA8XwI
z3!k^2O-+ox@p|rVJ$5Q4RJ++@*P(;4vyR9fmHx9TzxSDoV(0p&FSaFf*4oW+ja({5
zZFCEd$0sBbwh^~lzG*G0sBhe}`y_qse6U=2&gv+CZuIYeuh<@#^tH193yFIdv~^M4
zSF-~)KAu;Zxhbr<_&d{rn!MG<m2Ga0Wj)K&yPwoL-R(@zwO)lF)^S)ECn~Y4qE#9f
z$60XJ=)y^}ol{qsm{!d%4E7TBFwgFGx8d*=`ObNMXkXJQz5ZH-vF!Do*%1xp2hFbU
z_I1@4Q?~tC#uvJbZOgy+ogRFc(w(8()u6gj8BtVK9r}mrLK1Im{Nd=%iOIl>UsL(f
zlb#z(s|;#oXpP2@ykqjbf6mP+LzrjWn!$w1zChm)3D0@2H+yinJT~D6mp0SD$ds4V
zVM7Val6iG%FluUlP8YJFj%9Ibwk#};J=FTlDZ4qlX43^rK%<8&E%<PO%JfU9PutY#
z`2o&kj(!EpSui>1u1veG>b%ir=(0`au#akXw;QfZY;hN>>K5xSrWdatFrTiuvU1T9
z?y+`*&MEcY%Uw6(9l~;xOqPvC$xow;#=Fg`AI}AkIb{sHR(1!?%ig@S(=T&z=bL1u
zeaI*KJOfu36|l{QV8QX|!1yY+&Gt6#jjmfR6<a3-z4EwtUs7s}y6xs`mYps1%h|Hb
zO6+Y`8ERvEgztUx#vBSbBzcCFR`DiTxb+q_tK^nqF01H3ac{ja&;HCn%)>P?$|<Mi
zr8)=x$k?W#FKbIL3A#EG+}blw?%z>o^H?&PHJ*3@H{5)1`<`lEbzgqV-TR1Ze^X&N
z=A|^1r02eW+uLGF*g1d4+y|^ZEm4Q8qjZ9z>p$BcQ1Co2=Q~PE?)fj2A;;)vEe+gH
zpR|0)C5<PP*uf>Uo}#^1wI1wsqgL$KmWJ?iL4QgM^lR%o4l85FPOwMi&LhjbO*(3S
qf7ZlhNsqqpLE_tj54HN9a%I1pxPwF6&3--!{t5&O7llzFc-sGd_j+*v
--- a/mobile/android/base/resources/layout/tab_queue_prompt.xml
+++ b/mobile/android/base/resources/layout/tab_queue_prompt.xml
@@ -67,19 +67,19 @@
             android:id="@+id/bottom_container"
             android:layout_width="match_parent"
             android:layout_height="52dp"
             android:layout_gravity="center"
             android:layout_marginBottom="40dp">
 
             <ImageView
                 android:id="@+id/enabled_confirmation"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
                 android:src="@drawable/img_check"
                 android:visibility="gone" />
 
             <LinearLayout
                 android:id="@+id/button_container"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:gravity="center_horizontal"
new file mode 100644
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -334,10 +334,32 @@ this.BrowserTestUtils = {
   /**
    *  Version of synthesizeMouse that uses a client point within the child
    *  window instead of a target as the offset. Otherwise, the arguments and
    *  return value are the same as synthesizeMouse.
    */
   synthesizeMouseAtPoint(offsetX, offsetY, event, browser)
   {
     return BrowserTestUtils.synthesizeMouse(null, offsetX, offsetY, event, browser);
+  },
+
+  /**
+   * Removes the given tab from its parent tabbrowser and
+   * waits until its final message has reached the parent.
+   */
+  removeTab(tab, options = {}) {
+    let dontRemove = options && options.dontRemove;
+
+    return new Promise(resolve => {
+      let {messageManager: mm, frameLoader} = tab.linkedBrowser;
+      mm.addMessageListener("SessionStore:update", function onMessage(msg) {
+        if (msg.targetFrameLoader == frameLoader && msg.data.isFinal) {
+          mm.removeMessageListener("SessionStore:update", onMessage);
+          resolve();
+        }
+      }, true);
+
+      if (!dontRemove && !tab.closing) {
+        tab.ownerDocument.defaultView.gBrowser.removeTab(tab);
+      }
+    });
   }
 };
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -875,16 +875,18 @@ this.PlacesUtils = {
            aItemId == PlacesUtils.tagsFolderId ||
            aItemId == PlacesUtils.placesRootId;
   },
 
   /**
    * Set the POST data associated with a bookmark, if any.
    * Used by POST keywords.
    *   @param aBookmarkId
+   *
+   * @deprecated Use PlacesUtils.keywords.insert() API instead.
    */
   setPostDataForBookmark(aBookmarkId, aPostData) {
     if (!aPostData)
       throw new Error("Must provide valid POST data");
     // For now we don't have a unified API to create a keyword with postData,
     // thus here we can just try to complete a keyword that should already exist
     // without any post data.
     let stmt = PlacesUtils.history.DBConnection.createStatement(
@@ -918,16 +920,18 @@ this.PlacesUtils = {
       }
     }).catch(Cu.reportError);
   },
 
   /**
    * Get the POST data associated with a bookmark, if any.
    * @param aBookmarkId
    * @returns string of POST data if set for aBookmarkId. null otherwise.
+   *
+   * @deprecated Use PlacesUtils.keywords.fetch() API instead.
    */
   getPostDataForBookmark(aBookmarkId) {
     let stmt = PlacesUtils.history.DBConnection.createStatement(
       `SELECT k.post_data
        FROM moz_keywords k
        JOIN moz_places h ON h.id = k.place_id
        JOIN moz_bookmarks b ON b.fk = h.id
        WHERE b.id = :item_id`);
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -528,28 +528,34 @@ interface nsINavBookmarksService : nsISu
   void getBookmarkIdsForURI(in nsIURI aURI, [optional] out unsigned long count,
                             [array, retval, size_is(count)] out long long bookmarks);
 
   /**
    * Associates the given keyword with the given bookmark.
    *
    * Use an empty keyword to clear the keyword associated with the URI.
    * In both of these cases, succeeds but does nothing if the URL/keyword is not found.
+   *
+   * @deprecated Use PlacesUtils.keywords.insert() API instead.
    */
   void setKeywordForBookmark(in long long aItemId, in AString aKeyword);
 
   /**
    * Retrieves the keyword for the given bookmark. Will be void string
    * (null in JS) if no such keyword is found.
+   *
+   * @deprecated Use PlacesUtils.keywords.fetch() API instead.
    */
   AString getKeywordForBookmark(in long long aItemId);
 
   /**
    * Returns the URI associated with the given keyword. Empty if no such
    * keyword is found.
+   *
+   * @deprecated Use PlacesUtils.keywords.fetch() API instead.
    */
   nsIURI getURIForKeyword(in AString keyword);
 
   /**
    * Adds a bookmark observer. If ownsWeak is false, the bookmark service will
    * keep an owning reference to the observer.  If ownsWeak is true, then
    * aObserver must implement nsISupportsWeakReference, and the bookmark
    * service will keep a weak reference to the observer.
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -2440,16 +2440,18 @@ nsNavBookmarks::GetKeywordForBookmark(in
 NS_IMETHODIMP
 nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword,
                                  nsIURI** aURI)
 {
   NS_ENSURE_ARG_POINTER(aURI);
   NS_ENSURE_TRUE(!aUserCasedKeyword.IsEmpty(), NS_ERROR_INVALID_ARG);
   *aURI = nullptr;
 
+  PLACES_WARN_DEPRECATED();
+
   // Shortcuts are always lowercased internally.
   nsAutoString keyword(aUserCasedKeyword);
   ToLowerCase(keyword);
 
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
     "SELECT h.url "
     "FROM moz_places h "
     "JOIN moz_keywords k ON k.place_id = h.id "
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -397,21 +397,16 @@
             this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"]
                                        .createInstance(Components.interfaces.nsITypeAheadFind);
             this._fastFind.init(this.docShell);
           }
           return this._fastFind;
         ]]></getter>
       </property>
 
-      <field name="_permanentKey">({})</field>
-
-      <property name="permanentKey" readonly="true"
-                onget="return this._permanentKey;"/>
-
       <property name="outerWindowID" readonly="true">
         <getter><![CDATA[
           return this.contentWindow
                      .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                      .getInterface(Components.interfaces.nsIDOMWindowUtils)
                      .outerWindowID;
         ]]></getter>
       </property>
@@ -1091,18 +1086,17 @@
           // Fields which are built as a result of notifactions (pageshow/hide,
           // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
           // because these notifications are dispatched again once the docshells
           // are swapped.
           var fieldsToSwap = [
             "_docShell",
             "_webBrowserFind",
             "_contentWindow",
-            "_webNavigation",
-            "_permanentKey"
+            "_webNavigation"
           ];
 
           if (this.isRemoteBrowser) {
             fieldsToSwap.push(...[
               "_remoteWebNavigation",
               "_remoteWebProgressManager",
               "_remoteWebProgress",
               "_remoteFinder",
--- a/toolkit/content/widgets/editor.xml
+++ b/toolkit/content/widgets/editor.xml
@@ -129,19 +129,22 @@
       <property name="editortype"
                 onget="return this.getAttribute('editortype');"
                 onset="this.setAttribute('editortype', val); return val;"/>
       <property name="webNavigation"
                 onget="return this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);"
                 readonly="true"/>
       <property name="contentDocument" readonly="true"
                 onget="return this.webNavigation.document;"/>
-      <property name="docShell"
-                onget="return this.boxObject.docShell;"
-                readonly="true"/>
+      <property name="docShell" readonly="true">
+        <getter><![CDATA[
+          let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+          return frameLoader ? frameLoader.docShell : null;
+        ]]></getter>
+      </property>
       <property name="currentURI"
                 readonly="true"
                 onget="return this.webNavigation.currentURI;"/>
       <property name="contentWindow"
                 readonly="true"
                 onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);"/>
       <property name="contentWindowAsCPOW"
                 readonly="true"
--- a/toolkit/content/widgets/general.xml
+++ b/toolkit/content/widgets/general.xml
@@ -80,19 +80,22 @@
           this._lightweightTheme = null;
         }
       ]]></destructor>
     </implementation>
   </binding>
 
   <binding id="iframe" role="outerdoc">
     <implementation>
-      <property name="docShell"
-                readonly="true"
-                onget="return this.boxObject.docShell"/>
+      <property name="docShell" readonly="true">
+        <getter><![CDATA[
+          let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+          return frameLoader ? frameLoader.docShell : null;
+        ]]></getter>
+      </property>
       <property name="contentWindow"
                 readonly="true"
                 onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);"/>
       <property name="webNavigation"
                 onget="return this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);"
                 readonly="true"/>
       <property name="contentDocument" readonly="true"
                 onget="return this.webNavigation.document;"/>
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -4741,16 +4741,20 @@ function BreakpointActor(aThreadActor, a
   this.condition = null;
   this.isPending = true;
 }
 
 BreakpointActor.prototype = {
   actorPrefix: "breakpoint",
   condition: null,
 
+  disconnect: function () {
+    this.removeScripts();
+  },
+
   hasScript: function (aScript) {
     return this.scripts.has(aScript);
   },
 
   /**
    * Called when this same breakpoint is added to another Debugger.Script
    * instance.
    *
--- a/toolkit/modules/tests/browser/browser_RemotePageManager.js
+++ b/toolkit/modules/tests/browser/browser_RemotePageManager.js
@@ -44,16 +44,26 @@ function waitForPage(pages) {
       waitForMessage(target, "RemotePage:Load").then(() => resolve(target));
     }
 
     pages.addMessageListener("RemotePage:Init", listener);
     gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
   });
 }
 
+function swapDocShells(browser1, browser2) {
+  // Swap frameLoaders.
+  browser1.swapDocShells(browser2);
+
+  // Swap permanentKeys.
+  let tmp = browser1.permanentKey;
+  browser1.permanentKey = browser2.permanentKey;
+  browser2.permanentKey = tmp;
+}
+
 // Test that opening a page creates a port, sends the load event and then
 // navigating to a new page sends the unload event. Going back should create a
 // new port
 add_task(function* init_navigate() {
   let port = yield waitForPort(TEST_URL);
   is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
 
   let loaded = new Promise(resolve => {
@@ -188,34 +198,34 @@ add_task(function* browser_switch() {
   is(message.data.value, "om nom", "Should have the right cookie");
 
   port1.addMessageListener("Cookie", failOnMessage);
   port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
   message = yield waitForMessage(port2, "Cookie");
   port1.removeMessageListener("Cookie", failOnMessage);
   is(message.data.value, "om nom nom", "Should have the right cookie");
 
-  browser1.swapDocShells(browser2);
+  swapDocShells(browser1, browser2);
   is(port1.browser, browser2, "Should have noticed the swap");
   is(port2.browser, browser1, "Should have noticed the swap");
 
   // Cookies should have stayed the same
   port2.addMessageListener("Cookie", failOnMessage);
   port1.sendAsyncMessage("GetCookie");
   message = yield waitForMessage(port1, "Cookie");
   port2.removeMessageListener("Cookie", failOnMessage);
   is(message.data.value, "om nom", "Should have the right cookie");
 
   port1.addMessageListener("Cookie", failOnMessage);
   port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
   message = yield waitForMessage(port2, "Cookie");
   port1.removeMessageListener("Cookie", failOnMessage);
   is(message.data.value, "om nom nom", "Should have the right cookie");
 
-  browser1.swapDocShells(browser2);
+  swapDocShells(browser1, browser2);
   is(port1.browser, browser1, "Should have noticed the swap");
   is(port2.browser, browser2, "Should have noticed the swap");
 
   // Cookies should have stayed the same
   port2.addMessageListener("Cookie", failOnMessage);
   port1.sendAsyncMessage("GetCookie");
   message = yield waitForMessage(port1, "Cookie");
   port2.removeMessageListener("Cookie", failOnMessage);