Merge fx-team to m-c a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Thu, 29 Jan 2015 15:27:17 -0800
changeset 226643 29b05d283b0096018505a07b97472dbc80b1ed67
parent 226618 0de614a1d5f1439f22f9e5c62e66e5484979ca5d (current diff)
parent 226642 a465bbe65e90202d1c22ca7945304f56c3ed3a18 (diff)
child 226657 791f9d8707337c5d8eed97a1a227d511091cff0a
child 226701 0a50c8ae0aac7750a0a535689d463400e9c64b2a
child 226719 f5a4769477bf7880c88ab688ef4d7f94d8219369
push id28202
push userkwierso@gmail.com
push dateThu, 29 Jan 2015 23:27:24 +0000
treeherdermozilla-central@29b05d283b00 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
29b05d283b00 / 38.0a1 / 20150130030232 / files
nightly linux64
29b05d283b00 / 38.0a1 / 20150130030232 / files
nightly mac
29b05d283b00 / 38.0a1 / 20150130030232 / files
nightly win32
29b05d283b00 / 38.0a1 / 20150130030232 / files
nightly win64
29b05d283b00 / 38.0a1 / 20150130030232 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge CLOSED TREE
browser/base/content/content.js
browser/components/sessionstore/test/browser_500328.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -236,16 +236,18 @@ pref("extensions.dss.switchPending", fal
 
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.name", "chrome://browser/locale/browser.properties");
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description", "chrome://browser/locale/browser.properties");
 
 pref("lightweightThemes.update.enabled", true);
 pref("lightweightThemes.getMoreURL", "https://addons.mozilla.org/%LOCALE%/firefox/themes");
 pref("lightweightThemes.recommendedThemes", "[{\"id\":\"recommended-1\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/a-web-browser-renaissance/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.footer.jpg\",\"textcolor\":\"#000000\",\"accentcolor\":\"#f2d9b1\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.preview.jpg\",\"author\":\"Sean.Martell\",\"version\":\"0\"},{\"id\":\"recommended-2\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/space-fantasy/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.header.jpg\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.footer.jpg\",\"textcolor\":\"#ffffff\",\"accentcolor\":\"#d9d9d9\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.preview.jpg\",\"author\":\"fx5800p\",\"version\":\"1.0\"},{\"id\":\"recommended-3\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/linen-light/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.header.png\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.footer.png\",\"textcolor\":\"#None\",\"accentcolor\":\"#ada8a8\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.icon.png\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/3.preview.png\",\"author\":\"DVemer\",\"version\":\"1.0\"},{\"id\":\"recommended-4\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/pastel-gradient/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.header.png\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.footer.png\",\"textcolor\":\"#000000\",\"accentcolor\":\"#000000\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.icon.png\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.preview.png\",\"author\":\"darrinhenein\",\"version\":\"1.0\"},{\"id\":\"recommended-5\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/carbon-light/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.header.png\",\"footerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.footer.png\",\"textcolor\":\"#3b3b3b\",\"accentcolor\":\"#2e2e2e\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/5.preview.jpg\",\"author\":\"Jaxivo\",\"version\":\"1.0\"}]");
 
+pref("browser.eme.ui.enabled", false);
+
 // UI tour experience.
 pref("browser.uitour.enabled", true);
 pref("browser.uitour.loglevel", "Error");
 pref("browser.uitour.requireSecure", true);
 pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/themes/");
 pref("browser.uitour.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tour/");
 
 pref("browser.customizemode.tip0.shown", false);
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -506,22 +506,25 @@ let AboutReaderListener = {
         break;
 
       case "pageshow":
         if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader) {
           return;
         }
 
         ReaderMode.parseDocument(content.document).then(article => {
+          // Do nothing if there is no article, or if the content window has been destroyed.
+          if (article === null || content === null) {
+            return;
+          }
+
           // The loaded page may have changed while we were parsing the document.
           // Make sure we've got the current one.
           let currentURL = Services.io.newURI(content.document.documentURI, null, null).specIgnoringRef;
-
-          // Do nothing if there's no article or the page in this tab has changed.
-          if (article == null || (article.url != currentURL)) {
+          if (article.url !== currentURL) {
             return;
           }
 
           this._savedArticle = article;
           sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
 
         }).catch(e => Cu.reportError("Error parsing document: " + e));
         break;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -482,8 +482,11 @@ skip-if = e10s # Bug 1100687 - test dire
 [browser_e10s_switchbrowser.js]
 [browser_blockHPKP.js]
 skip-if = e10s # bug 1100687 - test directly manipulates content (content.document.getElementById)
 [browser_mcb_redirect.js]
 skip-if = e10s # bug 1084504 - [e10s] Mixed content detection does not take redirection into account
 [browser_windowactivation.js]
 [browser_contextmenu_childprocess.js]
 [browser_bug963945.js]
+[browser_readerMode.js]
+support-files =
+  readerModeArticle.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_readerMode.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that the reader mode button appears and works properly on reader-able content.
+
+const PREF = "reader.parse-on-load.enabled";
+
+const TEST_PATH = "http://example.com/browser/browser/base/content/test/general/";
+
+let readerButton = document.getElementById("reader-mode-button");
+
+add_task(function* () {
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref(PREF);
+    while (gBrowser.tabs.length > 1) {
+      gBrowser.removeCurrentTab();
+    }
+  });
+
+  // Enable the reader mode button.
+  Services.prefs.setBoolPref(PREF, true);
+
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+  is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
+
+  // Point tab to a test page that is reader-able.
+  let url = TEST_PATH + "readerModeArticle.html";
+  yield promiseTabLoadEvent(tab, url);
+  yield promiseWaitForCondition(() => !readerButton.hidden);
+  is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
+
+  readerButton.click();
+  yield promiseTabLoadEvent(tab);
+
+  ok(gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader"), "about:reader loaded after clicking reader mode button");
+  is_element_visible(readerButton, "Reader mode button is present on about:reader");
+
+  readerButton.click();
+  yield promiseTabLoadEvent(tab);
+  is(gBrowser.selectedBrowser.currentURI.spec, url, "Original page loaded after clicking active reader mode button");
+
+  // Load a new tab that is NOT reader-able.
+  let newTab = gBrowser.selectedTab = gBrowser.addTab();
+  yield promiseTabLoadEvent(newTab, TEST_PATH + "download_page.html");
+  yield promiseWaitForCondition(() => readerButton.hidden);
+  is_element_hidden(readerButton, "Reader mode button is not present on a non-reader-able page");
+
+  // Switch back to the original tab to make sure reader mode button is still visible.
+  gBrowser.removeCurrentTab();
+  yield promiseWaitForCondition(() => !readerButton.hidden);
+  is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/readerModeArticle.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
--- a/browser/components/preferences/content.js
+++ b/browser/components/preferences/content.js
@@ -1,33 +1,36 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 /* 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/. */
 
 var gContentPane = {
-
-  /**
-   * Initializes the fonts dropdowns displayed in this pane.
-   */
   init: function ()
   {
+    // Initializes the fonts dropdowns displayed in this pane.
     this._rebuildFonts();
     var menulist = document.getElementById("defaultFont");
     if (menulist.selectedIndex == -1) {
       menulist.insertItemAt(0, "", "", "");
       menulist.selectedIndex = 0;
     }
 
     // Show translation preferences if we may:
     const prefName = "browser.translation.ui.show";
     if (Services.prefs.getBoolPref(prefName)) {
       let row = document.getElementById("translationBox");
       row.removeAttribute("hidden");
     }
+
+    let drmInfoURL =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
+    document.getElementById("playDRMContentLink").setAttribute("href", drmInfoURL);
+    document.getElementById("playDRMContentRow").hidden =
+      !Services.prefs.getBoolPref("browser.eme.ui.enabled");
   },
 
   // UTILITY FUNCTIONS
 
   /**
    * Utility function to enable/disable the button specified by aButtonID based
    * on the value of the Boolean preference specified by aPreferenceID.
    */
--- a/browser/components/preferences/content.xul
+++ b/browser/components/preferences/content.xul
@@ -17,16 +17,19 @@
 
   <prefpane id="paneContent" 
             onpaneload="gContentPane.init();"
             helpTopic="prefs-content">
 
     <preferences id="contentPreferences">
       <!--XXX buttons prefs -->
 
+      <!-- DRM content -->
+      <preference id="media.eme.enabled" name="media.eme.enabled" type="bool"/>
+
       <!-- POPUPS -->
       <preference id="dom.disable_open_during_load"   name="dom.disable_open_during_load"   type="bool"/>
 
       <!-- FONTS -->
       <preference id="font.language.group"
                   name="font.language.group"
                   type="wstring"
                   onchange="gContentPane._rebuildFonts();"/>
@@ -45,26 +48,37 @@
     <!-- various checkboxes, font-fu -->
     <groupbox id="miscGroup">
       <grid id="contentGrid">
         <columns>
           <column flex="1"/>
           <column/>
         </columns>
         <rows id="contentRows-1">
+          <row id="playDRMContentRow">
+            <vbox align="start">
+              <checkbox id="playDRMContent" preference="media.eme.enabled"
+                        label="&playDRMContent.label;" accesskey="&playDRMContent.accesskey;"/>
+            </vbox>
+            <hbox pack="end">
+              <label id="playDRMContentLink" class="text-link" value="&playDRMContent.learnMore.label;"/>
+            </hbox>
+          </row>
           <row id="popupPolicyRow">
             <vbox align="start">
               <checkbox id="popupPolicy" preference="dom.disable_open_during_load"
                         label="&blockPopups.label;" accesskey="&blockPopups.accesskey;"
                         onsyncfrompreference="return gContentPane.updateButtons('popupPolicyButton', 
                                                                             'dom.disable_open_during_load');"/>
             </vbox>
-            <button id="popupPolicyButton" label="&popupExceptions.label;"
-                    oncommand="gContentPane.showPopupExceptions();"
-                    accesskey="&popupExceptions.accesskey;"/>
+            <hbox pack="end">
+              <button id="popupPolicyButton" label="&popupExceptions.label;"
+                      oncommand="gContentPane.showPopupExceptions();"
+                      accesskey="&popupExceptions.accesskey;"/>
+            </hbox>
           </row>
         </rows>
       </grid>
     </groupbox>
 
     <!-- Fonts and Colors -->
     <groupbox id="fontsGroup">
       <caption label="&fontsAndColors.label;"/>
--- a/browser/components/preferences/in-content/applications.js
+++ b/browser/components/preferences/in-content/applications.js
@@ -1501,38 +1501,46 @@ var gApplicationsPane = {
                                                         this._brandShortName]);
       pluginMenuItem.setAttribute("label", label);
       pluginMenuItem.setAttribute("tooltiptext", label);
       pluginMenuItem.setAttribute(APP_ICON_ATTR_NAME, "plugin");
       menuPopup.appendChild(pluginMenuItem);
     }
 
     // Create a menu item for selecting a local application.
+    let canOpenWithOtherApp = true;
 #ifdef XP_WIN
     // On Windows, selecting an application to open another application
     // would be meaningless so we special case executables.
-    var executableType = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService)
+    let executableType = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService)
                                                   .getTypeFromExtension("exe");
-    if (handlerInfo.type != executableType)
+    canOpenWithOtherApp = handlerInfo.type != executableType;
 #endif
+    if (canOpenWithOtherApp)
     {
       let menuItem = document.createElement("menuitem");
-      menuItem.setAttribute("oncommand", "gApplicationsPane.chooseApp(event)");
+      menuItem.className = "choose-app-item";
+      menuItem.addEventListener("command", function(e) {
+        gApplicationsPane.chooseApp(e);
+      });
       let label = this._prefsBundle.getString("useOtherApp");
       menuItem.setAttribute("label", label);
       menuItem.setAttribute("tooltiptext", label);
       menuPopup.appendChild(menuItem);
     }
 
     // Create a menu item for managing applications.
     if (possibleAppMenuItems.length) {
       let menuItem = document.createElement("menuseparator");
       menuPopup.appendChild(menuItem);
       menuItem = document.createElement("menuitem");
-      menuItem.setAttribute("oncommand", "gApplicationsPane.manageApp(event)");
+      menuItem.className = "manage-app-item";
+      menuItem.addEventListener("command", function(e) {
+        gApplicationsPane.manageApp(e);
+      });
       menuItem.setAttribute("label", this._prefsBundle.getString("manageApp"));
       menuPopup.appendChild(menuItem);
     }
 
     // Select the item corresponding to the preferred action.  If the always
     // ask flag is set, it overrides the preferred action.  Otherwise we pick
     // the item identified by the preferred action (when the preferred action
     // is to use a helper app, we have to pick the specific helper app item).
@@ -1695,30 +1703,33 @@ var gApplicationsPane = {
   manageApp: function(aEvent) {
     // Don't let the normal "on select action" handler get this event,
     // as we handle it specially ourselves.
     aEvent.stopPropagation();
 
     var typeItem = this._list.selectedItem;
     var handlerInfo = this._handledTypes[typeItem.type];
 
-    gSubDialog.open("chrome://browser/content/preferences/applicationManager.xul",
-                    "resizable=no", handlerInfo);
-
-    // Rebuild the actions menu so that we revert to the previous selection,
-    // or "Always ask" if the previous default application has been removed
-    this.rebuildActionsMenu();
+    let onComplete = () => {
+      // Rebuild the actions menu so that we revert to the previous selection,
+      // or "Always ask" if the previous default application has been removed
+      this.rebuildActionsMenu();
 
-    // update the richlistitem too. Will be visible when selecting another row
-    typeItem.setAttribute("actionDescription",
-                          this._describePreferredAction(handlerInfo));
-    if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) {
-      typeItem.setAttribute("actionIcon",
-                            this._getIconURLForPreferredAction(handlerInfo));
-    }
+      // update the richlistitem too. Will be visible when selecting another row
+      typeItem.setAttribute("actionDescription",
+                            this._describePreferredAction(handlerInfo));
+      if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) {
+        typeItem.setAttribute("actionIcon",
+                              this._getIconURLForPreferredAction(handlerInfo));
+      }
+    };
+
+    gSubDialog.open("chrome://browser/content/preferences/applicationManager.xul",
+                    "resizable=no", handlerInfo, onComplete);
+
   },
 
   chooseApp: function(aEvent) {
     // Don't let the normal "on select action" handler get this event,
     // as we handle it specially ourselves.
     aEvent.stopPropagation();
 
     var handlerApp;
@@ -1757,27 +1768,30 @@ var gApplicationsPane = {
       params.mimeInfo = handlerInfo.wrappedHandlerInfo;
     }
 
     params.title         = this._prefsBundle.getString("fpTitleChooseApp");
     params.description   = handlerInfo.description;
     params.filename      = null;
     params.handlerApp    = null;
 
-    gSubDialog.open("chrome://global/content/appPicker.xul",
-                    null, params);
-
-    if (this.isValidHandlerApp(params.handlerApp)) {
-      handlerApp = params.handlerApp;
+    let onAppSelected = () => {
+      if (this.isValidHandlerApp(params.handlerApp)) {
+        handlerApp = params.handlerApp;
 
-      // Add the app to the type's list of possible handlers.
-      handlerInfo.addPossibleApplicationHandler(handlerApp);
-    }
+        // Add the app to the type's list of possible handlers.
+        handlerInfo.addPossibleApplicationHandler(handlerApp);
+      }
 
-    chooseAppCallback(handlerApp);
+      chooseAppCallback(handlerApp);
+    };
+
+    gSubDialog.open("chrome://global/content/appPicker.xul",
+                    null, params, onAppSelected);
+
 #else
     let winTitle = this._prefsBundle.getString("fpTitleChooseApp");
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     let fpCallback = function fpCallback_done(aResult) {
       if (aResult == Ci.nsIFilePicker.returnOK && fp.file &&
           this._isValidHandlerExecutable(fp.file)) {
         handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
                      createInstance(Ci.nsILocalHandlerApp);
--- a/browser/components/preferences/in-content/content.js
+++ b/browser/components/preferences/in-content/content.js
@@ -1,25 +1,22 @@
 /* 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/. */
 
 var gContentPane = {
-
-  /**
-   * Initializes the fonts dropdowns displayed in this pane.
-   */
   init: function ()
   {
     function setEventListener(aId, aEventType, aCallback)
     {
       document.getElementById(aId)
               .addEventListener(aEventType, aCallback.bind(gContentPane));
     }
 
+    // Initializes the fonts dropdowns displayed in this pane.
     this._rebuildFonts();
     var menulist = document.getElementById("defaultFont");
     if (menulist.selectedIndex == -1) {
       menulist.insertItemAt(0, "", "", "");
       menulist.selectedIndex = 0;
     }
 
     // Show translation preferences if we may:
@@ -38,16 +35,22 @@ var gContentPane = {
     setEventListener("colors", "command",
       gContentPane.configureColors);
     setEventListener("chooseLanguage", "command",
       gContentPane.showLanguages);
     setEventListener("translationAttributionImage", "click",
       gContentPane.openTranslationProviderAttribution);
     setEventListener("translateButton", "command",
       gContentPane.showTranslationExceptions);
+
+    let drmInfoURL =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
+    document.getElementById("playDRMContentLink").setAttribute("href", drmInfoURL);
+    document.getElementById("playDRMContentRow").hidden =
+      !Services.prefs.getBoolPref("browser.eme.ui.enabled");
   },
 
   // UTILITY FUNCTIONS
 
   /**
    * Utility function to enable/disable the button specified by aButtonID based
    * on the value of the Boolean preference specified by aPreferenceID.
    */
--- a/browser/components/preferences/in-content/content.xul
+++ b/browser/components/preferences/in-content/content.xul
@@ -1,16 +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/.
 
 <!-- Content panel -->
 
 <preferences id="contentPreferences" hidden="true" data-category="paneContent">
 
+  <!-- DRM content -->
+  <preference id="media.eme.enabled"
+              name="media.eme.enabled"
+              type="bool"/>
+
   <!-- Popups -->
   <preference id="dom.disable_open_during_load"
               name="dom.disable_open_during_load"
               type="bool"/>
 
   <!-- Fonts -->
   <preference id="font.language.group"
               name="font.language.group"
@@ -30,33 +35,42 @@
 <hbox id="header-content"
       class="header"
       hidden="true"
       data-category="paneContent">
   <label class="header-name">&paneContent.title;</label>
 </hbox>
 
 <groupbox id="miscGroup" data-category="paneContent" hidden="true">
-  <caption><label>&popups.label;</label></caption>
-
   <grid id="contentGrid">
     <columns>
       <column flex="1"/>
       <column/>
     </columns>
     <rows id="contentRows-1">
+      <row id="playDRMContentRow">
+        <vbox align="start">
+          <checkbox id="playDRMContent" preference="media.eme.enabled"
+                    label="&playDRMContent.label;" accesskey="&playDRMContent.accesskey;"/>
+        </vbox>
+        <hbox pack="end">
+          <label id="playDRMContentLink" class="text-link" value="&playDRMContent.learnMore.label;"/>
+        </hbox>
+      </row>
       <row id="popupPolicyRow">
         <vbox align="start">
           <checkbox id="popupPolicy" preference="dom.disable_open_during_load"
                     label="&blockPopups.label;" accesskey="&blockPopups.accesskey;"
                     onsyncfrompreference="return gContentPane.updateButtons('popupPolicyButton',
                                                                         'dom.disable_open_during_load');"/>
         </vbox>
-        <button id="popupPolicyButton" label="&popupExceptions.label;"
-                accesskey="&popupExceptions.accesskey;"/>
+        <hbox pack="end">
+          <button id="popupPolicyButton" label="&popupExceptions.label;"
+                  accesskey="&popupExceptions.accesskey;"/>
+        </hbox>
       </row>
     </rows>
   </grid>
 </groupbox>
 
 <!-- Fonts and Colors -->
 <groupbox id="fontsGroup" data-category="paneContent" hidden="true">
   <caption><label>&fontsAndColors.label;</label></caption>
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -5,16 +5,18 @@ support-files =
   privacypane_tests_perwindow.js
 
 [browser_advanced_update.js]
 [browser_bug410900.js]
 [browser_bug731866.js]
 [browser_bug795764_cachedisabled.js]
 [browser_bug1018066_resetScrollPosition.js]
 [browser_bug1020245_openPreferences_to_paneContent.js]
+[browser_change_app_handler.js]
+run-if = os == "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows
 [browser_connection.js]
 [browser_connection_bug388287.js]
 [browser_healthreport.js]
 skip-if = !healthreport || (os == 'linux' && debug)
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_4.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_change_app_handler.js
@@ -0,0 +1,92 @@
+let gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+let gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService);
+
+function setupFakeHandler() {
+  let info = gMimeSvc.getFromTypeAndExtension("text/plain", "foo.txt");
+  ok(info.possibleLocalHandlers.length, "Should have at least one known handler");
+  let handler = info.possibleLocalHandlers.queryElementAt(0, Ci.nsILocalHandlerApp);
+
+  let infoToModify = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
+  infoToModify.possibleApplicationHandlers.appendElement(handler, false);
+
+  gHandlerSvc.store(infoToModify);
+}
+
+add_task(function*() {
+  setupFakeHandler();
+  yield openPreferencesViaOpenPreferencesAPI("applications", null, {leaveOpen: true});
+  info("Preferences page opened on the applications pane.");
+  let win = gBrowser.selectedBrowser.contentWindow;
+
+  let container = win.document.getElementById("handlersView");
+  let ourItem = container.querySelector("richlistitem[type='text/x-test-handler']");
+  ok(ourItem, "handlersView is present");
+  ourItem.scrollIntoView();
+  container.selectItem(ourItem);
+  ok(ourItem.selected, "Should be able to select our item.");
+
+  let list = yield waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu"));
+  info("Got list after item was selected");
+
+  let chooseItem = list.firstChild.querySelector(".choose-app-item");
+  let dialogLoadedPromise = promiseLoadSubDialog("chrome://global/content/appPicker.xul");
+  chooseItem.click();
+
+  let dialog = yield dialogLoadedPromise;
+  info("Dialog loaded");
+
+  let dialogDoc = dialog.document;
+  let dialogList = dialogDoc.getElementById("app-picker-listbox");
+  dialogList.selectItem(dialogList.firstChild);
+  let selectedApp = dialogList.firstChild.handlerApp;
+  dialogDoc.documentElement.acceptDialog();
+
+  // Verify results are correct in mime service:
+  let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
+  ok(mimeInfo.preferredApplicationHandler.equals(selectedApp), "App should be set as preferred.");
+
+  // Check that we display this result:
+  list = yield waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu"));
+  info("Got list after item was selected");
+  ok(list.selectedItem, "Should have a selected item");
+  ok(mimeInfo.preferredApplicationHandler.equals(list.selectedItem.handlerApp),
+     "App should be visible as preferred item.");
+
+
+  // Now try to 'manage' this list:
+  dialogLoadedPromise = promiseLoadSubDialog("chrome://browser/content/preferences/applicationManager.xul");
+
+  let manageItem = list.firstChild.querySelector(".manage-app-item");
+  manageItem.click();
+
+  dialog = yield dialogLoadedPromise;
+  info("Dialog loaded the second time");
+
+  dialogDoc = dialog.document;
+  dialogList = dialogDoc.getElementById("appList");
+  let itemToRemove = dialogList.querySelector('listitem[label="' + selectedApp.name + '"]');
+  dialogList.selectItem(itemToRemove);
+  let itemsBefore = dialogList.children.length;
+  dialogDoc.getElementById("remove").click();
+  ok(!itemToRemove.parentNode, "Item got removed from DOM");
+  is(dialogList.children.length, itemsBefore - 1, "Item got removed");
+  dialogDoc.documentElement.acceptDialog();
+
+  // Verify results are correct in mime service:
+  mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
+  ok(!mimeInfo.preferredApplicationHandler, "App should no longer be set as preferred.");
+
+  // Check that we display this result:
+  list = yield waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu"));
+  ok(list.selectedItem, "Should have a selected item");
+  ok(!list.selectedItem.handlerApp,
+     "No app should be visible as preferred item.");
+
+  gBrowser.removeCurrentTab();
+});
+
+registerCleanupFunction(function() {
+  let infoToModify = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
+  gHandlerSvc.remove(infoToModify);
+});
+
--- a/browser/components/preferences/in-content/tests/head.js
+++ b/browser/components/preferences/in-content/tests/head.js
@@ -34,45 +34,48 @@ function open_preferences(aCallback) {
   let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
   newTabBrowser.addEventListener("Initialized", function () {
     newTabBrowser.removeEventListener("Initialized", arguments.callee, true);
     aCallback(gBrowser.contentWindow);
   }, true);
 }
 
 function openAndLoadSubDialog(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
-  let dialog = content.gSubDialog.open(aURL, aFeatures, aParams, aClosingCallback);
-  let deferred = Promise.defer();
+  let promise = promiseLoadSubDialog(aURL);
+  content.gSubDialog.open(aURL, aFeatures, aParams, aClosingCallback);
+  return promise;
+}
 
-  content.gSubDialog._frame.addEventListener("load", function load(aEvent) {
-    if (aEvent.target.contentWindow.location == "about:blank")
-      return;
-    content.gSubDialog._frame.removeEventListener("load", load);
+function promiseLoadSubDialog(aURL) {
+  return new Promise((resolve, reject) => {
+    content.gSubDialog._frame.addEventListener("load", function load(aEvent) {
+      if (aEvent.target.contentWindow.location == "about:blank")
+        return;
+      content.gSubDialog._frame.removeEventListener("load", load);
 
-    ise(content.gSubDialog._frame.contentWindow.location.toString(), aURL,
-        "Check the proper URL is loaded");
-
-    // Check visibility
-    is_element_visible(content.gSubDialog._overlay, "Overlay is visible");
+      ise(content.gSubDialog._frame.contentWindow.location.toString(), aURL,
+          "Check the proper URL is loaded");
 
-    // Check that stylesheets were injected
-    let expectedStyleSheetURLs = content.gSubDialog._injectedStyleSheets.slice(0);
-    for (let styleSheet of content.gSubDialog._frame.contentDocument.styleSheets) {
-      let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
-      if (i >= 0) {
-        info("found " + styleSheet.href);
-        expectedStyleSheetURLs.splice(i, 1);
+      // Check visibility
+      is_element_visible(content.gSubDialog._overlay, "Overlay is visible");
+
+      // Check that stylesheets were injected
+      let expectedStyleSheetURLs = content.gSubDialog._injectedStyleSheets.slice(0);
+      for (let styleSheet of content.gSubDialog._frame.contentDocument.styleSheets) {
+        let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
+        if (i >= 0) {
+          info("found " + styleSheet.href);
+          expectedStyleSheetURLs.splice(i, 1);
+        }
       }
-    }
-    ise(expectedStyleSheetURLs.length, 0, "All expectedStyleSheetURLs should have been found");
+      ise(expectedStyleSheetURLs.length, 0, "All expectedStyleSheetURLs should have been found");
 
-    deferred.resolve(dialog);
+      resolve(content.gSubDialog._frame.contentWindow);
+    });
   });
-
-  return deferred.promise;
 }
 
 /**
  * Waits a specified number of miliseconds for a specified event to be
  * fired on a specified element.
  *
  * Usage:
  *    let receivedEvent = waitForEvent(element, "eventName");
@@ -134,8 +137,29 @@ function openPreferencesViaOpenPreferenc
       if (!aOptions || !aOptions.leaveOpen)
         gBrowser.removeCurrentTab();
       deferred.resolve({selectedPane: selectedPane, selectedAdvancedTab: selectedAdvancedTab});
     });
   }, true);
 
   return deferred.promise;
 }
+
+function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) {
+  return new Promise((resolve, reject) => {
+    function tryNow() {
+      tries++;
+      let rv = aConditionFn();
+      if (rv) {
+        resolve(rv);
+      } else if (tries < aMaxTries) {
+        tryAgain();
+      } else {
+        reject("Condition timed out: " + aConditionFn.toSource());
+      }
+    }
+    function tryAgain() {
+      setTimeout(tryNow, aCheckInterval);
+    }
+    let tries = 0;
+    tryAgain();
+  });
+}
--- a/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
@@ -95,17 +95,17 @@ function test() {
     // setup a state for tab (A) so we can check later that is restored
     let key = "key";
     let value = "Value " + Math.random();
     let state = { entries: [{ url: testURL }], extData: { key: value } };
 
     // public session, add new tab: (A)
     let tab_A = aWin.gBrowser.addTab(testURL);
     ss.setTabState(tab_A, JSON.stringify(state));
-    whenBrowserLoaded(tab_A.linkedBrowser, function() {
+    promiseBrowserLoaded(tab_A.linkedBrowser).then(() => {
       // make sure that the next closed tab will increase getClosedTabCount
       Services.prefs.setIntPref(
         "browser.sessionstore.max_tabs_undo", max_tabs_undo + 1)
 
       // populate tab_A with form data
       for (let i in fieldList)
         setFormValue(tab_A, i, fieldList[i]);
 
@@ -114,42 +114,41 @@ function test() {
 
       // verify that closedTabCount increased
       ok(ss.getClosedTabCount(aWin) > count,
          "getClosedTabCount has increased after closing a tab");
 
       // verify tab: (A), in undo list
       let tab_A_restored = test(function() ss.undoCloseTab(aWin, 0));
       ok(tab_A_restored, "a tab is in undo list");
-      whenTabRestored(tab_A_restored, function() {
+      promiseTabRestored(tab_A_restored).then(() => {
         is(testURL, tab_A_restored.linkedBrowser.currentURI.spec,
            "it's the same tab that we expect");
         aWin.gBrowser.removeTab(tab_A_restored);
 
         whenNewWindowLoaded({ private: true }, function(aWin) {
           windowsToClose.push(aWin);
 
           // setup a state for tab (B) so we can check that its duplicated
           // properly
           let key1 = "key1";
           let value1 = "Value " + Math.random();
           let state1 = {
             entries: [{ url: testURL2 }], extData: { key1: value1 }
           };
 
           let tab_B = aWin.gBrowser.addTab(testURL2);
-          ss.setTabState(tab_B, JSON.stringify(state1));
-          whenTabRestored(tab_B, function() {
+          promiseTabState(tab_B, state1).then(() => {
             // populate tab: (B) with different form data
             for (let item in fieldList)
               setFormValue(tab_B, item, fieldList[item]);
 
             // duplicate tab: (B)
             let tab_C = aWin.gBrowser.duplicateTab(tab_B);
-            whenTabRestored(tab_C, function() {
+            promiseTabRestored(tab_C).then(() => {
               // verify the correctness of the duplicated tab
               is(ss.getTabValue(tab_C, key1), value1,
                 "tab successfully duplicated - correct state");
 
               for (let item in fieldList)
                 ok(compareFormValue(tab_C, item, fieldList[item]),
                   "The value for \"" + item + "\" was correctly duplicated");
 
--- a/browser/components/sessionstore/test/browser_339445.js
+++ b/browser/components/sessionstore/test/browser_339445.js
@@ -6,23 +6,23 @@ function test() {
   /** Test for Bug 339445 **/
 
   waitForExplicitFinish();
 
   let testURL = "http://mochi.test:8888/browser/" +
     "browser/components/sessionstore/test/browser_339445_sample.html";
 
   let tab = gBrowser.addTab(testURL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
+  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
     let doc = tab.linkedBrowser.contentDocument;
     is(doc.getElementById("storageTestItem").textContent, "PENDING",
        "sessionStorage value has been set");
 
     let tab2 = gBrowser.duplicateTab(tab);
-    whenTabRestored(tab2, function() {
+    promiseTabRestored(tab2).then(() => {
       let doc2 = tab2.linkedBrowser.contentDocument;
       is(doc2.getElementById("storageTestItem").textContent, "SUCCESS",
          "sessionStorage value has been duplicated");
 
       // clean up
       gBrowser.removeTab(tab2);
       gBrowser.removeTab(tab);
 
--- a/browser/components/sessionstore/test/browser_350525.js
+++ b/browser/components/sessionstore/test/browser_350525.js
@@ -66,32 +66,32 @@ function test() {
   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);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
+  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);
 
     // remove tab
     gBrowser.removeTab(tab);
 
     // getClosedTabCount
     var newcount = ss.getClosedTabCount(window);
     ok(newcount > count, "after closing a tab, getClosedTabCount has been incremented");
 
     // undoCloseTab
     tab = test(function() ss.undoCloseTab(window, 0));
     ok(tab, "undoCloseTab doesn't throw")
 
-    whenTabRestored(tab, function() {
+    promiseTabRestored(tab).then(() => {
       is(tab.linkedBrowser.currentURI.spec, testURL, "correct tab was reopened");
 
       // clean up
       if (gPrefService.prefHasUserValue("browser.sessionstore.max_tabs_undo"))
         gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
       gBrowser.removeTab(tab);
       finish();
     });
--- a/browser/components/sessionstore/test/browser_367052.js
+++ b/browser/components/sessionstore/test/browser_367052.js
@@ -9,22 +9,21 @@ function test() {
 
   // 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);
 
   // restore a blank tab
   let tab = gBrowser.addTab("about:");
-  whenBrowserLoaded(tab.linkedBrowser, function() {
+  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
     let history = tab.linkedBrowser.webNavigation.sessionHistory;
     ok(history.count >= 1, "the new tab does have at least one history entry");
 
-    ss.setTabState(tab, JSON.stringify({ entries: [] }));
-    whenTabRestored(tab, function() {
+    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");
 
       gBrowser.removeTab(tab);
       ok(ss.getClosedTabCount(window) == closedTabCount,
          "The closed blank tab wasn't added to Recently Closed Tabs");
--- a/browser/components/sessionstore/test/browser_394759_basic.js
+++ b/browser/components/sessionstore/test/browser_394759_basic.js
@@ -15,19 +15,17 @@ const TEST_URL = "data:text/html;charset
 function test() {
   waitForExplicitFinish();
 
   let uniqueKey = "bug 394759";
   let uniqueValue = "unik" + Date.now();
   let uniqueText = "pi != " + Math.random();
 
   // Clear the list of closed windows.
-  while (SessionStore.getClosedWindowCount()) {
-    SessionStore.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
 
   provideWindow(function onTestURLLoaded(newWin) {
     newWin.gBrowser.addTab().linkedBrowser.stop();
 
     // mark the window with some unique data to be restored later on
     ss.setWindowValue(newWin, uniqueKey, uniqueValue);
     let [txt, chk] = newWin.content.document.querySelectorAll("#txt, #chk");
     txt.value = uniqueText;
--- a/browser/components/sessionstore/test/browser_394759_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_394759_perwindowpb.js
@@ -79,19 +79,17 @@ function promiseBlankState() {
 
     // Make sure that sessionstore.js can be forced to be created by setting
     // the interval pref to 0.
     yield forceSaveState();
   });
 }
 
 add_task(function* init() {
-  while (ss.getClosedWindowCount() > 0) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
   while (ss.getClosedTabCount(window) > 0) {
     ss.forgetClosedTab(window, 0);
   }
 });
 
 add_task(function* main() {
   yield promiseTestOpenCloseWindow(false, TESTS[0]);
   yield promiseTestOpenCloseWindow(true, TESTS[1]);
--- a/browser/components/sessionstore/test/browser_423132.js
+++ b/browser/components/sessionstore/test/browser_423132.js
@@ -24,17 +24,17 @@ function test() {
   // make sure sessionstore saves the cookie data, then close the window
   newWin.addEventListener("load", function (aEvent) {
     newWin.removeEventListener("load", arguments.callee, false);
 
     // Wait for sessionstore to be ready to restore this window
     executeSoon(function() {
       newWin.gBrowser.loadURI(testURL, null, null);
 
-      whenBrowserLoaded(newWin.gBrowser.selectedBrowser, function() {
+      promiseBrowserLoaded(newWin.gBrowser.selectedBrowser).then(() => {
         // get the sessionstore state for the window
         TabState.flush(newWin.gBrowser.selectedBrowser);
         let state = ss.getWindowState(newWin);
 
         // verify our cookie got set during pageload
         let e = cs.enumerator;
         let cookie;
         let i = 0;
--- a/browser/components/sessionstore/test/browser_447951.js
+++ b/browser/components/sessionstore/test/browser_447951.js
@@ -13,24 +13,23 @@ function test() {
   gPrefService.setIntPref("browser.sessionstore.max_serialize_back", -1);
   gPrefService.setIntPref("browser.sessionstore.max_serialize_forward", -1);
   registerCleanupFunction(function () {
     gPrefService.clearUserPref("browser.sessionstore.max_serialize_back");
     gPrefService.clearUserPref("browser.sessionstore.max_serialize_forward");
   });
 
   let tab = gBrowser.addTab();
-  whenBrowserLoaded(tab.linkedBrowser, function() {
+  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
     let tabState = { entries: [] };
     let max_entries = gPrefService.getIntPref("browser.sessionhistory.max_entries");
     for (let i = 0; i < max_entries; i++)
       tabState.entries.push({ url: baseURL + i });
 
-    ss.setTabState(tab, JSON.stringify(tabState));
-    whenTabRestored(tab, function() {
+    promiseTabState(tab, tabState).then(() => {
       TabState.flush(tab.linkedBrowser);
       tabState = JSON.parse(ss.getTabState(tab));
       is(tabState.entries.length, max_entries, "session history filled to the limit");
       is(tabState.entries[0].url, baseURL + 0, "... but not more");
 
       // visit yet another anchor (appending it to session history)
       runInContent(tab.linkedBrowser, function(win) {
         win.document.querySelector("a").click();
--- a/browser/components/sessionstore/test/browser_463205.js
+++ b/browser/components/sessionstore/test/browser_463205.js
@@ -12,25 +12,23 @@ const URL = ROOT + "browser_463205_sampl
  */
 add_task(function test_check_urls_before_restoring() {
   // Add a blank tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Restore form data with a valid URL.
-  ss.setTabState(tab, getState(URL));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, getState(URL));
 
   let value = yield getInputValue(browser, {id: "text"});
   is(value, "foobar", "value was restored");
 
   // Restore form data with an invalid URL.
-  ss.setTabState(tab, getState("http://example.com/"));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, getState("http://example.com/"));
 
   value = yield getInputValue(browser, {id: "text"});
   is(value, "", "value was not restored");
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
--- a/browser/components/sessionstore/test/browser_463206.js
+++ b/browser/components/sessionstore/test/browser_463206.js
@@ -27,17 +27,17 @@ function test() {
     }
 
     let doc = tab.linkedBrowser.contentDocument;
     typeText(doc.getElementById("out1"), Date.now());
     typeText(doc.getElementsByName("1|#out2")[0], Math.random());
     typeText(doc.defaultView.frames[0].frames[1].document.getElementById("in1"), new Date());
 
     let tab2 = gBrowser.duplicateTab(tab);
-    whenTabRestored(tab2, function() {
+    promiseTabRestored(tab2).then(() => {
       let doc = tab2.linkedBrowser.contentDocument;
       let win = tab2.linkedBrowser.contentWindow;
       isnot(doc.getElementById("out1").value,
             win.frames[1].document.getElementById("out1").value,
             "text isn't reused for frames");
       isnot(doc.getElementsByName("1|#out2")[0].value, "",
             "text containing | and # is correctly restored");
       is(win.frames[1].document.getElementById("out2").value, "",
--- a/browser/components/sessionstore/test/browser_465215.js
+++ b/browser/components/sessionstore/test/browser_465215.js
@@ -8,29 +8,28 @@ function test() {
   waitForExplicitFinish();
 
   let uniqueName = "bug 465215";
   let uniqueValue1 = "as good as unique: " + Date.now();
   let uniqueValue2 = "as good as unique: " + Math.random();
 
   // set a unique value on a new, blank tab
   let tab1 = gBrowser.addTab();
-  whenBrowserLoaded(tab1.linkedBrowser, function() {
+  promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
     ss.setTabValue(tab1, uniqueName, uniqueValue1);
 
     // duplicate the tab with that value
     let tab2 = ss.duplicateTab(window, tab1);
     is(ss.getTabValue(tab2, uniqueName), uniqueValue1, "tab value was duplicated");
 
     ss.setTabValue(tab2, uniqueName, uniqueValue2);
     isnot(ss.getTabValue(tab1, uniqueName), uniqueValue2, "tab values aren't sync'd");
 
     // overwrite the tab with the value which should remove it
-    ss.setTabState(tab1, JSON.stringify({ entries: [] }));
-    whenTabRestored(tab1, function() {
+    promiseTabState(tab1, {entries: []}).then(() => {
       is(ss.getTabValue(tab1, uniqueName), "", "tab value was cleared");
 
       // clean up
       gBrowser.removeTab(tab2);
       gBrowser.removeTab(tab1);
       finish();
     });
   });
--- a/browser/components/sessionstore/test/browser_467409-backslashplosion.js
+++ b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
@@ -32,28 +32,25 @@ function createEntry(sessionData) {
 
 add_task(function test_nested_about_sessionrestore() {
   // Prepare a blank tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // test 1
-  ss.setTabState(tab, JSON.stringify(STATE));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, STATE);
   checkState("test1", tab);
 
   // test 2
-  ss.setTabState(tab, JSON.stringify(STATE2));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, STATE2);
   checkState("test2", tab);
 
   // test 3
-  ss.setTabState(tab, JSON.stringify(STATE3));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, STATE3);
   checkState("test3", tab);
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 function checkState(prefix, tab) {
   // Flush and query tab state.
--- a/browser/components/sessionstore/test/browser_480893.js
+++ b/browser/components/sessionstore/test/browser_480893.js
@@ -8,38 +8,38 @@ function test() {
   waitForExplicitFinish();
 
   // Test that starting a new session loads a blank page if Firefox is
   // configured to display a blank page at startup (browser.startup.page = 0)
   gPrefService.setIntPref("browser.startup.page", 0);
   let tab = gBrowser.addTab("about:sessionrestore");
   gBrowser.selectedTab = tab;
   let browser = tab.linkedBrowser;
-  whenBrowserLoaded(browser, function() {
+  promiseBrowserLoaded(browser).then(() => {
     let doc = browser.contentDocument;
 
     // click on the "Start New Session" button after about:sessionrestore is loaded
     doc.getElementById("errorCancel").click();
-    whenBrowserLoaded(browser, function() {
+    promiseBrowserLoaded(browser).then(() => {
       let doc = browser.contentDocument;
 
       is(doc.URL, "about:blank", "loaded page is about:blank");
 
       // Test that starting a new session loads the homepage (set to http://mochi.test:8888)
       // if Firefox is configured to display a homepage at startup (browser.startup.page = 1)
       let homepage = "http://mochi.test:8888/";
       gPrefService.setCharPref("browser.startup.homepage", homepage);
       gPrefService.setIntPref("browser.startup.page", 1);
       gBrowser.loadURI("about:sessionrestore");
-      whenBrowserLoaded(browser, function() {
+      promiseBrowserLoaded(browser).then(() => {
         let doc = browser.contentDocument;
 
         // click on the "Start New Session" button after about:sessionrestore is loaded
         doc.getElementById("errorCancel").click();
-        whenBrowserLoaded(browser, function() {
+        promiseBrowserLoaded(browser).then(() => {
           let doc = browser.contentDocument;
 
           is(doc.URL, homepage, "loaded page is the homepage");
 
           // close tab, restore default values and finish the test
           gBrowser.removeTab(tab);
           gPrefService.clearUserPref("browser.startup.page");
           gPrefService.clearUserPref("browser.startup.homepage");
--- a/browser/components/sessionstore/test/browser_485563.js
+++ b/browser/components/sessionstore/test/browser_485563.js
@@ -5,17 +5,17 @@
 function test() {
   /** Test for Bug 485563 **/
 
   waitForExplicitFinish();
 
   let uniqueValue = Math.random() + "\u2028Second line\u2029Second paragraph\u2027";
 
   let tab = gBrowser.addTab();
-  whenBrowserLoaded(tab.linkedBrowser, function() {
+  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
     ss.setTabValue(tab, "bug485563", uniqueValue);
     let tabState = JSON.parse(ss.getTabState(tab));
     is(tabState.extData["bug485563"], uniqueValue,
        "unicode line separator wasn't over-encoded");
     ss.deleteTabValue(tab, "bug485563");
     ss.setTabState(tab, JSON.stringify(tabState));
     is(ss.getTabValue(tab, "bug485563"), uniqueValue,
        "unicode line separator was correctly preserved");
--- a/browser/components/sessionstore/test/browser_491168.js
+++ b/browser/components/sessionstore/test/browser_491168.js
@@ -9,31 +9,30 @@ function test() {
 
   const REFERRER1 = "http://example.org/?" + Date.now();
   const REFERRER2 = "http://example.org/?" + Math.random();
 
   let tab = gBrowser.addTab();
   gBrowser.selectedTab = tab;
 
   let browser = tab.linkedBrowser;
-  whenBrowserLoaded(browser, function() {
+  promiseBrowserLoaded(browser).then(() => {
     let tabState = JSON.parse(ss.getTabState(tab));
     is(tabState.entries[0].referrer,  REFERRER1,
        "Referrer retrieved via getTabState matches referrer set via loadURI.");
 
     tabState.entries[0].referrer = REFERRER2;
-    ss.setTabState(tab, JSON.stringify(tabState));
 
-    whenTabRestored(tab, function(e) {
+    promiseTabState(tab, tabState).then(() => {
       is(window.content.document.referrer, REFERRER2, "document.referrer matches referrer set via setTabState.");
 
       gBrowser.removeTab(tab);
 
       let newTab = ss.undoCloseTab(window, 0);
-      whenTabRestored(newTab, function() {
+      promiseTabRestored(newTab).then(() => {
         is(window.content.document.referrer, REFERRER2, "document.referrer is still correct after closing and reopening the tab.");
         gBrowser.removeTab(newTab);
 
         finish();
       });
     });
   });
 
--- a/browser/components/sessionstore/test/browser_500328.js
+++ b/browser/components/sessionstore/test/browser_500328.js
@@ -78,20 +78,20 @@ function test() {
   waitForExplicitFinish();
 
   // We open a new blank window, let it load, and then load in
   // http://example.com.  We need to load the blank window first, otherwise the
   // docshell gets confused and doesn't have a current history entry.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
 
-  whenBrowserLoaded(browser, function() {
+  promiseBrowserLoaded(browser).then(() => {
     browser.loadURI("http://example.com", null, null);
 
-    whenBrowserLoaded(browser, function() {
+    promiseBrowserLoaded(browser).then(() => {
       // After these push/replaceState calls, the window should have three
       // history entries:
       //   testURL        (state object: null)          <-- oldest
       //   testURL        (state object: {obj1:1})
       //   testURL?page2  (state object: {obj3:/^a$/})  <-- newest
       function contentTest(win) {
         let history = win.history;
         history.pushState({obj1:1}, "title-obj1");
@@ -104,15 +104,13 @@ function test() {
         gBrowser.removeTab(tab);
 
         // Restore the state into a new tab.  Things don't work well when we
         // restore into the old tab, but that's not a real use case anyway.
         let tab2 = gBrowser.addTab("about:blank");
         ss.setTabState(tab2, state, true);
 
         // Run checkState() once the tab finishes loading its restored state.
-        whenTabRestored(tab2, function() {
-          checkState(tab2);
-        });
+        promiseTabRestored(tab2).then(() => checkState(tab2));
       });
     });
   });
 }
--- a/browser/components/sessionstore/test/browser_506482.js
+++ b/browser/components/sessionstore/test/browser_506482.js
@@ -42,17 +42,17 @@ function test() {
   // make sure sessionstore.js is saved ASAP on all events
   gPrefService.setIntPref(PREF_INTERVAL, 0);
 
   // get the initial sessionstore.js mtime (-1 if it doesn't exist yet)
   let mtime0 = getSessionstorejsModificationTime();
 
   // create and select a first tab
   let tab = gBrowser.addTab(TEST_URL);
-  whenBrowserLoaded(tab.linkedBrowser, function() {
+  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
     // step1: the above has triggered some saveStateDelayed(), sleep until
     // it's done, and get the initial sessionstore.js mtime
     setTimeout(function step1(e) {
       let mtime1 = getSessionstorejsModificationTime();
       isnot(mtime1, mtime0, "initial sessionstore.js update");
 
       // step2: test sessionstore.js is not updated on tab selection
       // or content scrolling
--- a/browser/components/sessionstore/test/browser_579868.js
+++ b/browser/components/sessionstore/test/browser_579868.js
@@ -1,19 +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/. */
 
 function test() {
+  waitForExplicitFinish();
+
   let tab1 = gBrowser.addTab("about:rights");
   let tab2 = gBrowser.addTab("about:mozilla");
-  whenBrowserLoaded(tab1.linkedBrowser, mainPart);
-  waitForExplicitFinish();
 
-  function mainPart() {
+  promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
     // Tell the session storer that the tab is pinned
     let newTabState = '{"entries":[{"url":"about:rights"}],"pinned":true,"userTypedValue":"Hello World!"}';
     ss.setTabState(tab1, newTabState);
 
     // Undo pinning
     gBrowser.unpinTab(tab1);
 
     // Close and restore tab
@@ -21,10 +21,10 @@ function test() {
     let savedState = JSON.parse(ss.getClosedTabData(window))[0].state;
     isnot(savedState.pinned, true, "Pinned should not be true");
     tab1 = ss.undoCloseTab(window, 0);
 
     isnot(tab1.pinned, true, "Should not be pinned");
     gBrowser.removeTab(tab1);
     gBrowser.removeTab(tab2);
     finish();
-  }
+  });
 }
--- a/browser/components/sessionstore/test/browser_579879.js
+++ b/browser/components/sessionstore/test/browser_579879.js
@@ -1,15 +1,15 @@
 function test() {
   waitForExplicitFinish();
 
   var tab1 = gBrowser.addTab("data:text/plain;charset=utf-8,foo");
   gBrowser.pinTab(tab1);
 
-  whenBrowserLoaded(tab1.linkedBrowser, function() {
+  promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
     var tab2 = gBrowser.addTab();
     gBrowser.pinTab(tab2);
 
     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");
--- a/browser/components/sessionstore/test/browser_586068-apptabs.js
+++ b/browser/components/sessionstore/test/browser_586068-apptabs.js
@@ -1,53 +1,56 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   let state = { windows: [{ tabs: [
     { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
     { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
     { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
     { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
   ], selected: 5 }] };
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    loadCount++;
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      loadCount++;
 
-    // We'll make sure that the loads we get come from pinned tabs or the
-    // the selected tab.
+      // We'll make sure that the loads we get come from pinned tabs or the
+      // the selected tab.
 
-    // get the tab
-    let tab;
-    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-      if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-        tab = window.gBrowser.tabs[i];
-    }
+      // get the tab
+      let tab;
+      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+          tab = window.gBrowser.tabs[i];
+      }
 
-    ok(tab.pinned || tab.selected,
-       "load came from pinned or selected tab");
+      ok(tab.pinned || tab.selected,
+         "load came from pinned or selected tab");
 
-    // We should get 4 loads: 3 app tabs + 1 normal selected tab
-    if (loadCount < 4)
-      return;
+      // We should get 4 loads: 3 app tabs + 1 normal selected tab
+      if (loadCount < 4)
+        return;
 
-    gProgressListener.unsetCallback();
-    executeSoon(next);
+      gProgressListener.unsetCallback();
+      resolve();
+    });
   });
 
-  yield ss.setBrowserState(JSON.stringify(state));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
+++ b/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
@@ -1,20 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 const PREF_RESTORE_PINNED_TABS_ON_DEMAND = "browser.sessionstore.restore_pinned_tabs_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
   Services.prefs.setBoolPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND, true);
 
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
     Services.prefs.clearUserPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND);
   });
 
@@ -23,28 +19,35 @@ function runTests() {
     { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
     { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
     { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
   ], selected: 5 }] };
 
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    // get the tab
-    let tab;
-    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-      if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-        tab = window.gBrowser.tabs[i];
-    }
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      // get the tab
+      let tab;
+      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+          tab = window.gBrowser.tabs[i];
+      }
 
-    // Check that the load only comes from the selected tab.
-    ok(tab.selected, "load came from selected tab");
-    is(aNeedRestore, 6, "six tabs left to restore");
-    is(aRestoring, 1, "one tab is restoring");
-    is(aRestored, 0, "no tabs have been restored, yet");
+      // Check that the load only comes from the selected tab.
+      ok(tab.selected, "load came from selected tab");
+      is(aNeedRestore, 6, "six tabs left to restore");
+      is(aRestoring, 1, "one tab is restoring");
+      is(aRestored, 0, "no tabs have been restored, yet");
 
-    gProgressListener.unsetCallback();
-    executeSoon(next);
+      gProgressListener.unsetCallback();
+      resolve();
+    });
   });
 
-  yield ss.setBrowserState(JSON.stringify(state));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
+++ b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   // The first state will be loaded using setBrowserState, followed by the 2nd
   // state also being loaded using setBrowserState, interrupting the first restore.
   let state1 = { windows: [
@@ -59,50 +55,57 @@ function runTests() {
 
   // interruptedAfter will be set after the selected tab from each window have loaded.
   let interruptedAfter = 0;
   let loadedWindow1 = false;
   let loadedWindow2 = false;
   let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    loadCount++;
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      loadCount++;
 
-    if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
-      loadedWindow1 = true;
-    if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
-      loadedWindow2 = true;
+      if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
+        loadedWindow1 = true;
+      if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
+        loadedWindow2 = true;
 
-    if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
-      interruptedAfter = loadCount;
-      ss.setBrowserState(JSON.stringify(state2));
-      return;
-    }
+      if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
+        interruptedAfter = loadCount;
+        ss.setBrowserState(JSON.stringify(state2));
+        return;
+      }
 
-    if (loadCount < numTabs + interruptedAfter)
-      return;
+      if (loadCount < numTabs + interruptedAfter)
+        return;
 
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs + interruptedAfter, "all tabs were restored");
-    is(aNeedRestore, 0, "there are no tabs left needing restore");
+      // We don't actually care about load order in this test, just that they all
+      // do load.
+      is(loadCount, numTabs + interruptedAfter, "all tabs were restored");
+      is(aNeedRestore, 0, "there are no tabs left needing restore");
 
-    // Remove the progress listener from this window, it will be removed from
-    // theWin when that window is closed (in setBrowserState).
-    gProgressListener.unsetCallback();
-    executeSoon(next);
+      // Remove the progress listener.
+      gProgressListener.unsetCallback();
+      resolve();
+    });
   });
 
   // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
   Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
     if (aTopic == "domwindowopened") {
       let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
       win.addEventListener("load", function onLoad() {
         win.removeEventListener("load", onLoad);
         Services.ww.unregisterNotification(observer);
         win.gBrowser.addTabsProgressListener(gProgressListener);
       });
     }
   });
 
-  yield ss.setBrowserState(JSON.stringify(state1));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state1));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseAllButPrimaryWindowClosed();
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-cascade.js
+++ b/browser/components/sessionstore/test/browser_586068-cascade.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   let state = { windows: [{ tabs: [
     { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
@@ -28,24 +24,31 @@ function runTests() {
     [2, 3, 1],
     [1, 3, 2],
     [0, 3, 3],
     [0, 2, 4],
     [0, 1, 5]
   ];
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    loadCount++;
-    let expected = expectedCounts[loadCount - 1];
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      loadCount++;
+      let expected = expectedCounts[loadCount - 1];
 
-    is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
-    is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
-    is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
+      is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
+      is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
+      is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
 
-    if (loadCount == state.windows[0].tabs.length) {
-      gProgressListener.unsetCallback();
-      executeSoon(next);
-    }
+      if (loadCount == state.windows[0].tabs.length) {
+        gProgressListener.unsetCallback();
+        resolve();
+      }
+    });
   });
 
-  yield ss.setBrowserState(JSON.stringify(state));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-multi_window.js
+++ b/browser/components/sessionstore/test/browser_586068-multi_window.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   // The first window will be put into the already open window and the second
   // window will be opened with _openWindowWithState, which is the source of the problem.
   let state = { windows: [
@@ -33,34 +29,42 @@ function runTests() {
         { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
       ],
       selected: 4
     }
   ] };
   let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    if (++loadCount == numTabs) {
-      // We don't actually care about load order in this test, just that they all
-      // do load.
-      is(loadCount, numTabs, "all tabs were restored");
-      is(aNeedRestore, 0, "there are no tabs left needing restore");
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      if (++loadCount == numTabs) {
+        // We don't actually care about load order in this test, just that they all
+        // do load.
+        is(loadCount, numTabs, "all tabs were restored");
+        is(aNeedRestore, 0, "there are no tabs left needing restore");
 
-      gProgressListener.unsetCallback();
-      executeSoon(next);
-    }
+        gProgressListener.unsetCallback();
+        resolve();
+      }
+    });
   });
 
   // We also want to catch the 2nd window, so we need to observe domwindowopened
   Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
     if (aTopic == "domwindowopened") {
       let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
       win.addEventListener("load", function onLoad() {
         win.removeEventListener("load", onLoad);
         Services.ww.unregisterNotification(observer);
         win.gBrowser.addTabsProgressListener(gProgressListener);
       });
     }
   });
 
-  yield ss.setBrowserState(JSON.stringify(state));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseAllButPrimaryWindowClosed();
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-reload.js
+++ b/browser/components/sessionstore/test/browser_586068-reload.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   let state = { windows: [{ tabs: [
     { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
@@ -22,33 +18,37 @@ function runTests() {
     { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
   ], selected: 1 }] };
 
   let loadCount = 0;
-  gBrowser.tabContainer.addEventListener("SSTabRestored", function onRestored(event) {
-    let tab = event.target;
-    let browser = tab.linkedBrowser;
-    let tabData = state.windows[0].tabs[loadCount++];
-
-    // double check that this tab was the right one
-    is(browser.currentURI.spec, tabData.entries[0].url,
-       "load " + loadCount + " - browser loaded correct url");
-    is(ss.getTabValue(tab, "uniq"), tabData.extData.uniq,
-       "load " + loadCount + " - correct tab was restored");
+  let promiseRestoringTabs = new Promise(resolve => {
+    gBrowser.tabContainer.addEventListener("SSTabRestored", function onRestored(event) {
+      let tab = event.target;
+      let browser = tab.linkedBrowser;
+      let tabData = state.windows[0].tabs[loadCount++];
 
-    if (loadCount == state.windows[0].tabs.length) {
-      gBrowser.tabContainer.removeEventListener("SSTabRestored", onRestored);
+      // double check that this tab was the right one
+      is(browser.currentURI.spec, tabData.entries[0].url,
+         "load " + loadCount + " - browser loaded correct url");
+      is(ss.getTabValue(tab, "uniq"), tabData.extData.uniq,
+         "load " + loadCount + " - correct tab was restored");
 
-      executeSoon(function () {
-        waitForBrowserState(TestRunner.backupState, finish);
-      });
-    } else {
-      // reload the next tab
-      gBrowser.browsers[loadCount].reload();
-    }
+      if (loadCount == state.windows[0].tabs.length) {
+        gBrowser.tabContainer.removeEventListener("SSTabRestored", onRestored);
+        resolve();
+      } else {
+        // reload the next tab
+        gBrowser.browsers[loadCount].reload();
+      }
+    });
   });
 
-  yield ss.setBrowserState(JSON.stringify(state));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-select.js
+++ b/browser/components/sessionstore/test/browser_586068-select.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   let state = { windows: [{ tabs: [
     { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
     { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
@@ -29,38 +25,45 @@ function runTests() {
     [3, 1, 2],
     [2, 1, 3],
     [1, 1, 4],
     [0, 1, 5]
   ];
   let tabOrder = [0, 5, 1, 4, 3, 2];
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    loadCount++;
-    let expected = expectedCounts[loadCount - 1];
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      loadCount++;
+      let expected = expectedCounts[loadCount - 1];
 
-    is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
-    is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
-    is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
+      is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
+      is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
+      is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
 
-    if (loadCount < state.windows[0].tabs.length) {
-      // double check that this tab was the right one
-      let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
-      let tab;
-      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-          tab = window.gBrowser.tabs[i];
-      }
+      if (loadCount < state.windows[0].tabs.length) {
+        // double check that this tab was the right one
+        let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
+        let tab;
+        for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+          if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+            tab = window.gBrowser.tabs[i];
+        }
 
-      is(ss.getTabValue(tab, "uniq"), expectedData,
-        "load " + loadCount + " - correct tab was restored");
+        is(ss.getTabValue(tab, "uniq"), expectedData,
+          "load " + loadCount + " - correct tab was restored");
 
-      // select the next tab
-      window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
-    } else {
-      gProgressListener.unsetCallback();
-      executeSoon(next);
-    }
+        // select the next tab
+        window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
+      } else {
+        gProgressListener.unsetCallback();
+        resolve();
+      }
+    });
   });
 
-  yield ss.setBrowserState(JSON.stringify(state));
-}
+  let backupState = ss.getBrowserState();
+  ss.setBrowserState(JSON.stringify(state));
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-window_state.js
+++ b/browser/components/sessionstore/test/browser_586068-window_state.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   // We'll use 2 states so that we can make sure calling setWindowState doesn't
   // wipe out currently restoring data.
   let state1 = { windows: [{ tabs: [
@@ -28,29 +24,36 @@ function runTests() {
     { entries: [{ url: "http://example.org#2" }] },
     { entries: [{ url: "http://example.org#3" }] },
     { entries: [{ url: "http://example.org#4" }] },
     { entries: [{ url: "http://example.org#5" }] }
   ] }] };
   let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    // When loadCount == 2, we'll also restore state2 into the window
-    if (++loadCount == 2) {
-      ss.setWindowState(window, JSON.stringify(state2), false);
-    }
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      // When loadCount == 2, we'll also restore state2 into the window
+      if (++loadCount == 2) {
+        ss.setWindowState(window, JSON.stringify(state2), false);
+      }
 
-    if (loadCount < numTabs) {
-      return;
-    }
+      if (loadCount < numTabs) {
+        return;
+      }
 
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
-    is(aNeedRestore, 0, "there are no tabs left needing restore");
+      // We don't actually care about load order in this test, just that they all
+      // do load.
+      is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
+      is(aNeedRestore, 0, "there are no tabs left needing restore");
 
-    gProgressListener.unsetCallback();
-    executeSoon(next);
+      gProgressListener.unsetCallback();
+      resolve();
+    });
   });
 
-  yield ss.setWindowState(window, JSON.stringify(state1), true);
-}
+  let backupState = ss.getBrowserState();
+  ss.setWindowState(window, JSON.stringify(state1), true);
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_586068-window_state_override.js
+++ b/browser/components/sessionstore/test/browser_586068-window_state_override.js
@@ -1,19 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
   });
 
   // We'll use 2 states so that we can make sure calling setWindowState doesn't
   // wipe out currently restoring data.
   let state1 = { windows: [{ tabs: [
@@ -28,29 +24,36 @@ function runTests() {
     { entries: [{ url: "http://example.org#2" }] },
     { entries: [{ url: "http://example.org#3" }] },
     { entries: [{ url: "http://example.org#4" }] },
     { entries: [{ url: "http://example.org#5" }] }
   ] }] };
   let numTabs = 2 + state2.windows[0].tabs.length;
 
   let loadCount = 0;
-  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
-    // When loadCount == 2, we'll also restore state2 into the window
-    if (++loadCount == 2) {
-      executeSoon(() => ss.setWindowState(window, JSON.stringify(state2), true));
-    }
+  let promiseRestoringTabs = new Promise(resolve => {
+    gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+      // When loadCount == 2, we'll also restore state2 into the window
+      if (++loadCount == 2) {
+        executeSoon(() => ss.setWindowState(window, JSON.stringify(state2), true));
+      }
 
-    if (loadCount < numTabs) {
-      return;
-    }
+      if (loadCount < numTabs) {
+        return;
+      }
 
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs, "all tabs were restored");
-    is(aNeedRestore, 0, "there are no tabs left needing restore");
+      // We don't actually care about load order in this test, just that they all
+      // do load.
+      is(loadCount, numTabs, "all tabs were restored");
+      is(aNeedRestore, 0, "there are no tabs left needing restore");
 
-    gProgressListener.unsetCallback();
-    executeSoon(next);
+      gProgressListener.unsetCallback();
+      resolve();
+    });
   });
 
-  yield ss.setWindowState(window, JSON.stringify(state1), true);
-}
+  let backupState = ss.getBrowserState();
+  ss.setWindowState(window, JSON.stringify(state1), true);
+  yield promiseRestoringTabs;
+
+  // Cleanup.
+  yield promiseBrowserState(backupState);
+});
--- a/browser/components/sessionstore/test/browser_588426.js
+++ b/browser/components/sessionstore/test/browser_588426.js
@@ -27,15 +27,15 @@ function newWindowWithState(state, callb
   let win = window.openDialog(getBrowserURL(), "_blank", opts);
 
   win.addEventListener("load", function onLoad() {
     win.removeEventListener("load", onLoad, false);
 
     executeSoon(function () {
       win.addEventListener("SSWindowStateReady", function onReady() {
         win.removeEventListener("SSWindowStateReady", onReady, false);
-        whenTabRestored(win.gBrowser.tabs[0], () => callback(win));
+        promiseTabRestored(win.gBrowser.tabs[0]).then(() => callback(win));
       }, false);
 
       ss.setWindowState(win, JSON.stringify(state), true);
     });
   }, false);
 }
--- a/browser/components/sessionstore/test/browser_589246.js
+++ b/browser/components/sessionstore/test/browser_589246.js
@@ -158,17 +158,17 @@ function onStateRestored(aSubject, aTopi
   // change this window's windowtype so that closing a new window will trigger
   // browser-lastwindow-close-granted.
   document.documentElement.setAttribute("windowtype", "navigator:testrunner");
 
   let newWin = openDialog(location, "_blank", "chrome,all,dialog=no", "http://example.com");
   newWin.addEventListener("load", function(aEvent) {
     newWin.removeEventListener("load", arguments.callee, false);
 
-    whenBrowserLoaded(newWin.gBrowser.selectedBrowser, function() {
+    promiseBrowserLoaded(newWin.gBrowser.selectedBrowser).then(() => {
       // pin this tab
       if (shouldPinTab)
         newWin.gBrowser.pinTab(newWin.gBrowser.selectedTab);
 
       newWin.addEventListener("unload", function () {
         newWin.removeEventListener("unload", arguments.callee, false);
         onWindowUnloaded();
       }, false);
--- a/browser/components/sessionstore/test/browser_597071.js
+++ b/browser/components/sessionstore/test/browser_597071.js
@@ -2,19 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Bug 597071 - Closed windows should only be resurrected when there is a single
  * popup window
  */
 add_task(function test_close_last_nonpopup_window() {
   // Purge the list of closed windows.
-  while (ss.getClosedWindowCount()) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
 
   let oldState = ss.getWindowState(window);
 
   let popupState = {windows: [
     {tabs: [{entries: []}], isPopup: true, hidden: "toolbar"}
   ]};
 
   // Set this window to be a popup.
--- a/browser/components/sessionstore/test/browser_624727.js
+++ b/browser/components/sessionstore/test/browser_624727.js
@@ -1,16 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  TestRunner.run();
-}
+let TEST_STATE = { windows: [{ tabs: [{ url: "about:blank" }] }] };
 
-function runTests() {
+add_task(function* () {
   function assertNumberOfTabs(num, msg) {
     is(gBrowser.tabs.length, num, msg);
   }
 
   function assertNumberOfPinnedTabs(num, msg) {
     is(gBrowser._numPinnedTabs, num, msg);
   }
 
@@ -24,18 +22,14 @@ function runTests() {
 
   let [tab1, tab2] = gBrowser.tabs;
   let linkedBrowser = tab1.linkedBrowser;
   gBrowser.pinTab(tab1);
   gBrowser.pinTab(tab2);
   assertNumberOfPinnedTabs(2, "both tabs are now pinned");
 
   // run the test
-  yield waitForBrowserState(
-    { windows: [{ tabs: [{ url: "about:blank" }] }] },
-    function () {
-      assertNumberOfTabs(1, "one tab left after setBrowserState()");
-      assertNumberOfPinnedTabs(0, "there are no pinned tabs");
-      is(gBrowser.tabs[0].linkedBrowser, linkedBrowser, "first tab's browser got re-used");
-      next();
-    }
-  );
-}
+  yield promiseBrowserState(TEST_STATE);
+
+  assertNumberOfTabs(1, "one tab left after setBrowserState()");
+  assertNumberOfPinnedTabs(0, "there are no pinned tabs");
+  is(gBrowser.tabs[0].linkedBrowser, linkedBrowser, "first tab's browser got re-used");
+});
--- a/browser/components/sessionstore/test/browser_625016.js
+++ b/browser/components/sessionstore/test/browser_625016.js
@@ -12,19 +12,17 @@ add_task(function* setup() {
   // closed window is now in _closedWindows.
 
   requestLongerTimeout(2);
 
   yield forceSaveState();
 
   // We'll clear all closed windows to make sure our state is clean
   // forgetClosedWindow doesn't trigger a delayed save
-  while (ss.getClosedWindowCount()) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
   is(ss.getClosedWindowCount(), 0, "starting with no closed windows");
 });
 
 add_task(function* new_window() {
   let newWin;
   try {
     newWin = yield promiseNewWindowLoaded();
     let tab = newWin.gBrowser.addTab("http://example.com/browser_625016.js?" + Math.random());
@@ -74,13 +72,11 @@ add_task(function* new_tab() {
   }
 });
 
 
 add_task(function* done() {
   // The API still represents the closed window as closed, so we can clear it
   // with the API, but just to make sure...
 //  is(ss.getClosedWindowCount(), 1, "1 closed window according to API");
-  while (ss.getClosedWindowCount()) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
   Services.prefs.clearUserPref("browser.sessionstore.interval");
 });
--- a/browser/components/sessionstore/test/browser_637020.js
+++ b/browser/components/sessionstore/test/browser_637020.js
@@ -13,53 +13,54 @@ const TEST_STATE = {
   }, {
     tabs: [
       { entries: [{ url: TEST_URL }] },
       { entries: [{ url: TEST_URL }] }
     ]
   }]
 };
 
-function test() {
-  TestRunner.run();
-}
-
 /**
  * This test ensures that windows that have just been restored will be marked
  * as dirty, otherwise _getCurrentState() might ignore them when collecting
  * state for the first time and we'd just save them as empty objects.
  *
  * The dirty state acts as a cache to not collect data from all windows all the
  * time, so at the beginning, each window must be dirty so that we collect
  * their state at least once.
  */
 
-function runTests() {
-  let win;
-
+add_task(function* test() {
   // Wait until the new window has been opened.
-  Services.obs.addObserver(function onOpened(subject) {
-    Services.obs.removeObserver(onOpened, "domwindowopened");
-    win = subject;
-    executeSoon(next);
-  }, "domwindowopened", false);
+  let promiseWindow = new Promise(resolve => {
+    Services.obs.addObserver(function onOpened(subject) {
+      Services.obs.removeObserver(onOpened, "domwindowopened");
+      resolve(subject);
+    }, "domwindowopened", false);
+  });
 
   // Set the new browser state that will
   // restore a window with two slowly loading tabs.
-  yield SessionStore.setBrowserState(JSON.stringify(TEST_STATE));
+  let backupState = SessionStore.getBrowserState();
+  SessionStore.setBrowserState(JSON.stringify(TEST_STATE));
+  let win = yield promiseWindow;
 
   // The window has now been opened. Check the state that is returned,
   // this should come from the cache while the window isn't restored, yet.
   info("the window has been opened");
   checkWindows();
 
   // The history has now been restored and the tabs are loading. The data must
   // now come from the window, if it's correctly been marked as dirty before.
-  yield whenDelayedStartupFinished(win, next);
+  yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
   info("the delayed startup has finished");
   checkWindows();
-}
+
+  // Cleanup.
+  yield promiseWindowClosed(win);
+  yield promiseBrowserState(backupState);
+});
 
 function checkWindows() {
   let state = JSON.parse(SessionStore.getBrowserState());
   is(state.windows[0].tabs.length, 2, "first window has two tabs");
   is(state.windows[1].tabs.length, 2, "second window has two tabs");
 }
--- a/browser/components/sessionstore/test/browser_662743.js
+++ b/browser/components/sessionstore/test/browser_662743.js
@@ -58,20 +58,18 @@ function test() {
 }
 
 function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
   let testURL =
     getRootDirectory(gTestPath) + "browser_662743_sample.html";
   let tab = gBrowser.addTab(testURL);
   let tabState = { entries: [{ url: testURL, formdata: aFormData}] };
 
-  whenBrowserLoaded(tab.linkedBrowser, function() {
-    ss.setTabState(tab, JSON.stringify(tabState));
-
-    whenTabRestored(tab, function() {
+  promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+    promiseTabState(tab, tabState).then(() => {
       let doc = tab.linkedBrowser.contentDocument;
       let select = doc.getElementById("select_id");
       let value = select.options[select.selectedIndex].value;
 
       // Flush to make sure we have the latest form data.
       TabState.flush(tab.linkedBrowser);
       let restoredTabState = JSON.parse(ss.getTabState(tab));
 
--- a/browser/components/sessionstore/test/browser_687710_2.js
+++ b/browser/components/sessionstore/test/browser_687710_2.js
@@ -25,17 +25,17 @@ function test()
 {
   waitForExplicitFinish();
 
   registerCleanupFunction(function () {
     ss.setBrowserState(stateBackup);
   });
 
   let tab = gBrowser.addTab("about:blank");
-  waitForTabState(tab, state, function () {
+  promiseTabState(tab, state).then(() => {
     let history = tab.linkedBrowser.webNavigation.sessionHistory;
 
     is(history.count, 2, "history.count");
     for (let i = 0; i < history.count; i++) {
       for (let j = 0; j < history.count; j++) {
         compareEntries(i, j, history);
       }
     }
--- a/browser/components/sessionstore/test/browser_705597.js
+++ b/browser/components/sessionstore/test/browser_705597.js
@@ -13,25 +13,24 @@ function test() {
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("browser.sessionstore.interval");
   });
 
   let tab = gBrowser.addTab("about:blank");
 
   let browser = tab.linkedBrowser;
 
-  waitForTabState(tab, tabState, function () {
-
+  promiseTabState(tab, tabState).then(() => {
     let sessionHistory = browser.sessionHistory;
     let entry = sessionHistory.getEntryAtIndex(0, false);
     entry.QueryInterface(Ci.nsISHContainer);
 
     whenChildCount(entry, 1, function () {
       whenChildCount(entry, 2, function () {
-        whenBrowserLoaded(browser, function () {
+        promiseBrowserLoaded(browser).then(() => {
           TabState.flush(browser);
           let {entries} = JSON.parse(ss.getTabState(tab));
           is(entries.length, 1, "tab has one history entry");
           ok(!entries[0].children, "history entry has no subframes");
 
           // Make sure that we reset the state.
           let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
           waitForBrowserState(blankState, finish);
--- a/browser/components/sessionstore/test/browser_707862.js
+++ b/browser/components/sessionstore/test/browser_707862.js
@@ -13,25 +13,24 @@ function test() {
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("browser.sessionstore.interval");
   });
 
   let tab = gBrowser.addTab("about:blank");
 
   let browser = tab.linkedBrowser;
 
-  waitForTabState(tab, tabState, function() {
-
+  promiseTabState(tab, tabState).then(() => {
     let sessionHistory = browser.sessionHistory;
     let entry = sessionHistory.getEntryAtIndex(0, false);
     entry.QueryInterface(Ci.nsISHContainer);
 
     whenChildCount(entry, 1, function () {
       whenChildCount(entry, 2, function () {
-        whenBrowserLoaded(browser, function () {
+        promiseBrowserLoaded(browser).then(() => {
           let sessionHistory = browser.sessionHistory;
           let entry = sessionHistory.getEntryAtIndex(0, false);
 
           whenChildCount(entry, 0, function () {
             // Make sure that we reset the state.
             let blankState = { windows: [{ tabs: [{ entries: [{ url: "about:blank" }] }]}]};
             waitForBrowserState(blankState, finish);
           });
--- a/browser/components/sessionstore/test/browser_739805.js
+++ b/browser/components/sessionstore/test/browser_739805.js
@@ -13,42 +13,28 @@ function test() {
     if (gBrowser.tabs.length > 1)
       gBrowser.removeTab(gBrowser.tabs[1]);
     Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
   });
 
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
 
-  whenBrowserLoaded(browser, function () {
+  promiseBrowserLoaded(browser).then(() => {
     isnot(gBrowser.selectedTab, tab, "newly created tab is not selected");
 
     ss.setTabState(tab, JSON.stringify(tabState));
     is(browser.__SS_restoreState, TAB_STATE_NEEDS_RESTORE, "tab needs restoring");
 
     let state = JSON.parse(ss.getTabState(tab));
     let formdata = state.entries[0].formdata;
     is(formdata && formdata.id["foo"], "bar", "tab state's formdata is valid");
 
-    whenTabRestored(tab, function () {
+    promiseTabRestored(tab).then(() => {
       let input = browser.contentDocument.getElementById("foo");
       is(input.value, "bar", "formdata has been restored correctly");
       finish();
     });
 
     // Restore the tab by selecting it.
     gBrowser.selectedTab = tab;
   });
 }
-
-function whenBrowserLoaded(aBrowser, aCallback) {
-  aBrowser.addEventListener("load", function onLoad() {
-    aBrowser.removeEventListener("load", onLoad, true);
-    executeSoon(aCallback);
-  }, true);
-}
-
-function whenTabRestored(aTab, aCallback) {
-  aTab.addEventListener("SSTabRestored", function onRestored() {
-    aTab.removeEventListener("SSTabRestored", onRestored);
-    executeSoon(aCallback);
-  });
-}
--- a/browser/components/sessionstore/test/browser_819510_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_819510_perwindowpb.js
@@ -1,183 +1,116 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const originalState = ss.getBrowserState();
-
-/** Private Browsing Test for Bug 819510 **/
-function test() {
-  waitForExplicitFinish();
-  runNextTest();
-}
-
-let tests = [test_1, test_2, test_3 ];
-
-const testState = {
-  windows: [{
-    tabs: [
-      { entries: [{ url: "about:blank" }] },
-    ]
-  }]
-};
-
-function runNextTest() {
-  // Set an empty state
-  closeAllButPrimaryWindow();
-
-  // Run the next test, or finish
-  if (tests.length) {
-    let currentTest = tests.shift();
-    waitForBrowserState(testState, currentTest);
-  } else {
-    Services.obs.addObserver(
-      function observe(aSubject, aTopic, aData) {
-        Services.obs.removeObserver(observe, aTopic);
-        finish();
-      },
-      "sessionstore-browser-state-restored", false);
-    ss.setBrowserState(originalState);
-  }
-}
-
 // Test opening default mochitest-normal-private-normal-private windows
 // (saving the state with last window being private)
-function test_1() {
-  testOnWindow(false, function(aWindow) {
-    aWindow.gBrowser.addTab("http://www.example.com/1");
-    testOnWindow(true, function(aWindow) {
-      aWindow.gBrowser.addTab("http://www.example.com/2");
-      testOnWindow(false, function(aWindow) {
-        aWindow.gBrowser.addTab("http://www.example.com/3");
-        testOnWindow(true, function(aWindow) {
-          aWindow.gBrowser.addTab("http://www.example.com/4");
+add_task(function* test_1() {
+  let win = yield promiseNewWindowLoaded();
+  win.gBrowser.addTab("http://www.example.com/1");
 
-          let curState = JSON.parse(ss.getBrowserState());
-          is (curState.windows.length, 5, "Browser has opened 5 windows");
-          is (curState.windows[2].isPrivate, true, "Window is private");
-          is (curState.windows[4].isPrivate, true, "Last window is private");
-          is (curState.selectedWindow, 5, "Last window opened is the one selected");
+  win = yield promiseNewWindowLoaded({private: true});
+  win.gBrowser.addTab("http://www.example.com/2");
+
+  win = yield promiseNewWindowLoaded();
+  win.gBrowser.addTab("http://www.example.com/3");
+
+  win = yield promiseNewWindowLoaded({private: true});
+  win.gBrowser.addTab("http://www.example.com/4");
 
-          forceWriteState(function(state) {
-            is(state.windows.length, 3,
-               "sessionstore state: 3 windows in data being written to disk");
-            is (state.selectedWindow, 3,
-               "Selected window is updated to match one of the saved windows");
-            state.windows.forEach(function(win) {
-              is(!win.isPrivate, true, "Saved window is not private");
-            });
-            is(state._closedWindows.length, 0,
-               "sessionstore state: no closed windows in data being written to disk");
-            runNextTest();
-          });
-        });
-      });
-    });
-  });
-}
+  let curState = JSON.parse(ss.getBrowserState());
+  is(curState.windows.length, 5, "Browser has opened 5 windows");
+  is(curState.windows[2].isPrivate, true, "Window is private");
+  is(curState.windows[4].isPrivate, true, "Last window is private");
+  is(curState.selectedWindow, 5, "Last window opened is the one selected");
+
+  let state = JSON.parse(yield promiseRecoveryFileContents());
+
+  is(state.windows.length, 3,
+     "sessionstore state: 3 windows in data being written to disk");
+  is(state.selectedWindow, 3,
+     "Selected window is updated to match one of the saved windows");
+  ok(state.windows.every(win => !win.isPrivate),
+    "Saved windows are not private");
+  is(state._closedWindows.length, 0,
+     "sessionstore state: no closed windows in data being written to disk");
+
+  // Cleanup.
+  yield promiseAllButPrimaryWindowClosed();
+  forgetClosedWindows();
+});
 
 // Test opening default mochitest window + 2 private windows
-function test_2() {
-  testOnWindow(true, function(aWindow) {
-    aWindow.gBrowser.addTab("http://www.example.com/1");
-    testOnWindow(true, function(aWindow) {
-      aWindow.gBrowser.addTab("http://www.example.com/2");
+add_task(function* test_2() {
+  let win = yield promiseNewWindowLoaded({private: true});
+  win.gBrowser.addTab("http://www.example.com/1");
 
-      let curState = JSON.parse(ss.getBrowserState());
-      is (curState.windows.length, 3, "Browser has opened 3 windows");
-      is (curState.windows[1].isPrivate, true, "Window 1 is private");
-      is (curState.windows[2].isPrivate, true, "Window 2 is private");
-      is (curState.selectedWindow, 3, "Last window opened is the one selected");
+  win = yield promiseNewWindowLoaded({private: true});
+  win.gBrowser.addTab("http://www.example.com/2");
+
+  let curState = JSON.parse(ss.getBrowserState());
+  is(curState.windows.length, 3, "Browser has opened 3 windows");
+  is(curState.windows[1].isPrivate, true, "Window 1 is private");
+  is(curState.windows[2].isPrivate, true, "Window 2 is private");
+  is(curState.selectedWindow, 3, "Last window opened is the one selected");
 
-      forceWriteState(function(state) {
-        is(state.windows.length, 1,
-           "sessionstore state: 1 windows in data being written to disk");
-        is (state.selectedWindow, 1,
-           "Selected window is updated to match one of the saved windows");
-        is(state._closedWindows.length, 0,
-           "sessionstore state: no closed windows in data being written to disk");
-        runNextTest();
-      });
-    });
-  });
-}
+  let state = JSON.parse(yield promiseRecoveryFileContents());
+
+  is(state.windows.length, 1,
+     "sessionstore state: 1 windows in data being written to disk");
+  is(state.selectedWindow, 1,
+     "Selected window is updated to match one of the saved windows");
+  is(state._closedWindows.length, 0,
+     "sessionstore state: no closed windows in data being written to disk");
+
+  // Cleanup.
+  yield promiseAllButPrimaryWindowClosed();
+  forgetClosedWindows();
+});
 
 // Test opening default-normal-private-normal windows and closing a normal window
-function test_3() {
-  testOnWindow(false, function(normalWindow) {
-    waitForTabLoad(normalWindow, "http://www.example.com/", function() {
-      testOnWindow(true, function(aWindow) {
-        waitForTabLoad(aWindow, "http://www.example.com/", function() {
-          testOnWindow(false, function(aWindow) {
-            waitForTabLoad(aWindow, "http://www.example.com/", function() {
+add_task(function* test_3() {
+  let normalWindow = yield promiseNewWindowLoaded();
+  yield promiseTabLoad(normalWindow, "http://www.example.com/");
+
+  let win = yield promiseNewWindowLoaded({private: true});
+  yield promiseTabLoad(win, "http://www.example.com/");
+
+  win = yield promiseNewWindowLoaded();
+  yield promiseTabLoad(win, "http://www.example.com/");
 
-              let curState = JSON.parse(ss.getBrowserState());
-              is(curState.windows.length, 4, "Browser has opened 4 windows");
-              is(curState.windows[2].isPrivate, true, "Window 2 is private");
-              is(curState.selectedWindow, 4, "Last window opened is the one selected");
+  let curState = JSON.parse(ss.getBrowserState());
+  is(curState.windows.length, 4, "Browser has opened 4 windows");
+  is(curState.windows[2].isPrivate, true, "Window 2 is private");
+  is(curState.selectedWindow, 4, "Last window opened is the one selected");
+
+  yield promiseWindowClosed(normalWindow);
 
-              waitForWindowClose(normalWindow, function() {
-                // Pin and unpin a tab before checking the written state so that
-                // the list of restoring windows gets cleared. Otherwise the
-                // window we just closed would be marked as not closed.
-                let tab = aWindow.gBrowser.tabs[0];
-                aWindow.gBrowser.pinTab(tab);
-                aWindow.gBrowser.unpinTab(tab);
+  // Pin and unpin a tab before checking the written state so that
+  // the list of restoring windows gets cleared. Otherwise the
+  // window we just closed would be marked as not closed.
+  let tab = win.gBrowser.tabs[0];
+  win.gBrowser.pinTab(tab);
+  win.gBrowser.unpinTab(tab);
 
-                forceWriteState(function(state) {
-                  is(state.windows.length, 2,
-                     "sessionstore state: 2 windows in data being written to disk");
-                  is(state.selectedWindow, 2,
-                     "Selected window is updated to match one of the saved windows");
-                  state.windows.forEach(function(win) {
-                    is(!win.isPrivate, true, "Saved window is not private");
-                  });
-                  is(state._closedWindows.length, 1,
-                     "sessionstore state: 1 closed window in data being written to disk");
-                  state._closedWindows.forEach(function(win) {
-                    is(!win.isPrivate, true, "Closed window is not private");
-                  });
-                  runNextTest();
-                });
-              });
-            });
-          });
-        });
-      });
-    });
-  });
-}
+  let state = JSON.parse(yield promiseRecoveryFileContents());
 
-function waitForWindowClose(aWin, aCallback) {
-  let winCount = JSON.parse(ss.getBrowserState()).windows.length;
-  aWin.addEventListener("SSWindowClosing", function onWindowClosing() {
-    aWin.removeEventListener("SSWindowClosing", onWindowClosing, false);
-    function checkCount() {
-      let state = JSON.parse(ss.getBrowserState());
-      if (state.windows.length == (winCount - 1)) {
-        aCallback();
-      } else {
-        executeSoon(checkCount);
-      }
-    }
-    executeSoon(checkCount);
-  }, false);
-  aWin.close();
-}
+  is(state.windows.length, 2,
+     "sessionstore state: 2 windows in data being written to disk");
+  is(state.selectedWindow, 2,
+     "Selected window is updated to match one of the saved windows");
+  ok(state.windows.every(win => !win.isPrivate),
+    "Saved windows are not private");
+  is(state._closedWindows.length, 1,
+     "sessionstore state: 1 closed window in data being written to disk");
+  ok(state._closedWindows.every(win => !win.isPrivate),
+    "Closed windows are not private");
 
-function forceWriteState(aCallback) {
-  return promiseRecoveryFileContents().then(function(data) {
-    aCallback(JSON.parse(data));
-  });
-}
+  // Cleanup.
+  yield promiseAllButPrimaryWindowClosed();
+  forgetClosedWindows();
+});
 
-function testOnWindow(aIsPrivate, aCallback) {
-  whenNewWindowLoaded({private: aIsPrivate}, aCallback);
+function promiseTabLoad(win, url) {
+  let browser = win.gBrowser.selectedBrowser;
+  browser.loadURI(url);
+  return promiseBrowserLoaded(browser).then(() => TabState.flush(browser));
 }
-
-function waitForTabLoad(aWin, aURL, aCallback) {
-  let browser = aWin.gBrowser.selectedBrowser;
-  browser.loadURI(aURL);
-  whenBrowserLoaded(browser, function () {
-    TabState.flush(browser);
-    executeSoon(aCallback);
-  });
-}
--- a/browser/components/sessionstore/test/browser_911547.js
+++ b/browser/components/sessionstore/test/browser_911547.js
@@ -1,70 +1,52 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // This tests that session restore component does restore the right content
 // security policy with the document.
 // The policy being tested disallows inline scripts
 
-function test() {
-  TestRunner.run();
-}
-
-function runTests() {
+add_task(function* test() {
   // create a tab that has a CSP
   let testURL = "http://mochi.test:8888/browser/browser/components/sessionstore/test/browser_911547_sample.html";
   let tab = gBrowser.selectedTab = gBrowser.addTab(testURL);
   gBrowser.selectedTab = tab;
 
   let browser = tab.linkedBrowser;
-  yield waitForLoad(browser);
+  yield promiseBrowserLoaded(browser);
 
   // this is a baseline to ensure CSP is active
   // attempt to inject and run a script via inline (pre-restore, allowed)
   injectInlineScript(browser,'document.getElementById("test_id").value = "fail";');
   is(browser.contentDocument.getElementById("test_id").value, "ok",
      "CSP should block the inline script that modifies test_id");
 
   // attempt to click a link to a data: URI (will inherit the CSP of the
   // origin document) and navigate to the data URI in the link.
   browser.contentDocument.getElementById("test_data_link").click();
-  yield waitForLoad(browser);
+  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);
 
   // open new tab and recover the state
   tab = ss.undoCloseTab(window, 0);
-  yield waitForTabRestored(tab);
+  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");
 
   // clean up
   gBrowser.removeTab(tab);
-}
-
-function waitForLoad(aElement) {
-  aElement.addEventListener("load", function onLoad() {
-    aElement.removeEventListener("load", onLoad, true);
-    executeSoon(next);
-  }, true);
-}
-
-function waitForTabRestored(aElement) {
-  aElement.addEventListener("SSTabRestored", function tabRestored(e) {
-    aElement.removeEventListener("SSTabRestored", tabRestored, true);
-    executeSoon(next);
-  }, true);
-}
+});
 
 // injects an inline script element (with a text body)
 function injectInlineScript(browser, scriptText) {
   let scriptElt = browser.contentDocument.createElement("script");
   scriptElt.type = 'text/javascript';
   scriptElt.text = scriptText;
   browser.contentDocument.body.appendChild(scriptElt);
 }
--- a/browser/components/sessionstore/test/browser_attributes.js
+++ b/browser/components/sessionstore/test/browser_attributes.js
@@ -1,30 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  TestRunner.run();
-}
-
 /**
  * This test makes sure that we correctly preserve tab attributes when storing
  * and restoring tabs. It also ensures that we skip special attributes like
  * 'image' and 'pending' that need to be handled differently or internally.
  */
 
 const PREF = "browser.sessionstore.restore_on_demand";
 
-function runTests() {
+add_task(function* test() {
   Services.prefs.setBoolPref(PREF, true)
   registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
 
   // Add a new tab with a nice icon.
   let tab = gBrowser.addTab("about:robots");
-  yield whenBrowserLoaded(tab.linkedBrowser);
+  yield promiseBrowserLoaded(tab.linkedBrowser);
 
   // Check that the tab has an 'image' attribute.
   ok(tab.hasAttribute("image"), "tab.image exists");
 
   // Make sure we do not persist 'image' attributes.
   ss.persistTabAttribute("image");
   let {attributes} = JSON.parse(ss.getTabState(tab));
   ok(!("image" in attributes), "'image' attribute not saved");
@@ -39,34 +35,37 @@ function runTests() {
 
   // Make sure we're backwards compatible and restore old 'image' attributes.
   let state = {
     entries: [{url: "about:mozilla"}],
     attributes: {custom: "foobaz", image: gBrowser.getIcon(tab)}
   };
 
   // Prepare a pending tab waiting to be restored.
-  whenTabRestoring(tab);
-  yield ss.setTabState(tab, JSON.stringify(state));
+  let promise = promiseTabRestoring(tab);
+  ss.setTabState(tab, JSON.stringify(state));
+  yield promise;
 
   ok(tab.hasAttribute("pending"), "tab is pending");
   is(gBrowser.getIcon(tab), state.attributes.image, "tab has correct icon");
 
   // Let the pending tab load.
   gBrowser.selectedTab = tab;
-  yield whenTabRestored(tab);
+  yield promiseTabRestored(tab);
 
   // Ensure no 'image' or 'pending' attributes are stored.
   ({attributes} = JSON.parse(ss.getTabState(tab)));
   ok(!("image" in attributes), "'image' attribute not saved");
   ok(!("pending" in attributes), "'pending' attribute not saved");
   is(attributes.custom, "foobaz", "'custom' attribute is correct");
 
   // Clean up.
   gBrowser.removeTab(tab);
-}
+});
 
-function whenTabRestoring(tab) {
-  tab.addEventListener("SSTabRestoring", function onRestoring() {
-    tab.removeEventListener("SSTabRestoring", onRestoring);
-    executeSoon(next);
+function promiseTabRestoring(tab) {
+  return new Promise(resolve => {
+    tab.addEventListener("SSTabRestoring", function onRestoring() {
+      tab.removeEventListener("SSTabRestoring", onRestoring);
+      resolve();
+    });
   });
 }
--- a/browser/components/sessionstore/test/browser_broadcast.js
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -97,18 +97,17 @@ add_task(function flush_on_settabstate()
 
   let state = ss.getTabState(tab);
   yield modifySessionStorage(browser, {test: "on-set-tab-state"});
 
   // Flush all data contained in the content script but send it using
   // asynchronous messages.
   TabState.flushAsync(browser);
 
-  ss.setTabState(tab, state);
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, state);
 
   let {storage} = JSON.parse(ss.getTabState(tab));
   is(storage["http://example.com"].test, INITIAL_VALUE,
     "sessionStorage data has not been overwritten");
 
   gBrowser.removeTab(tab);
 });
 
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -36,30 +36,28 @@ add_task(function docshell_capabilities(
   // Check that we correctly save disallowed features.
   let disallowedState = JSON.parse(ss.getTabState(tab));
   let disallow = new Set(disallowedState.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Reuse the tab to restore a new, clean state into it.
-  ss.setTabState(tab, JSON.stringify({ entries: [{url: "about:robots"}] }));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, {entries: [{url: "about:robots"}]});
 
   // Flush to make sure chrome received all data.
   TabState.flush(browser);
 
   // After restoring disallowed features must be available again.
   state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed again");
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Restore the state with disallowed features.
-  ss.setTabState(tab, JSON.stringify(disallowedState));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, disallowedState);
 
   // Check that docShell flags are set.
   ok(!docShell.allowImages, "images not allowed");
   ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
 
   // Check that we correctly restored features as disabled.
   state = JSON.parse(ss.getTabState(tab));
   disallow = new Set(state.disallow.split(","));
--- a/browser/components/sessionstore/test/browser_cleaner.js
+++ b/browser/components/sessionstore/test/browser_cleaner.js
@@ -32,19 +32,17 @@ function promiseCleanup () {
 
 function getClosedState() {
   return Cu.cloneInto(CLOSED_STATE, {});
 }
 
 let CLOSED_STATE;
 
 add_task(function* init() {
-  while (ss.getClosedWindowCount() > 0) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
   while (ss.getClosedTabCount(window) > 0) {
     ss.forgetClosedTab(window, 0);
   }
 });
 
 add_task(function* test_open_and_close() {
   let newTab1 = gBrowser.addTab(URL_TAB1);
   yield promiseBrowserLoaded(newTab1.linkedBrowser);
--- a/browser/components/sessionstore/test/browser_dying_cache.js
+++ b/browser/components/sessionstore/test/browser_dying_cache.js
@@ -1,61 +1,55 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  TestRunner.run();
-}
-
 /**
  * This test ensures that after closing a window we keep its state data around
  * as long as something keeps a reference to it. It should only be possible to
  * read data after closing - writing should fail.
  */
 
-function runTests() {
+add_task(function* test() {
   // Open a new window.
-  let win = OpenBrowserWindow();
-  yield whenDelayedStartupFinished(win, next);
+  let win = yield promiseNewWindowLoaded();
 
   // Load some URL in the current tab.
   let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
   win.gBrowser.selectedBrowser.loadURIWithFlags("about:robots", flags);
-  yield whenBrowserLoaded(win.gBrowser.selectedBrowser);
+  yield promiseBrowserLoaded(win.gBrowser.selectedBrowser);
 
   // Open a second tab and close the first one.
   let tab = win.gBrowser.addTab("about:mozilla");
-  yield whenBrowserLoaded(tab.linkedBrowser);
+  yield promiseBrowserLoaded(tab.linkedBrowser);
   TabState.flush(tab.linkedBrowser);
   win.gBrowser.removeTab(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);
   let closedTabData = ss.getClosedTabData(win);
 
-  // Close our window and wait a tick.
-  whenWindowClosed(win);
-  yield win.close();
+  // Close our window.
+  yield promiseWindowClosed(win);
 
   // SessionStore should no longer track our window
   // but it should still report the same state.
   ok(!("__SSi" in win), "sessionstore does no longer track our window");
   checkWindowState(win);
 
   // Make sure we're not allowed to modify state data.
-  ok(shouldThrow(() => ss.setWindowState(win, {})),
-     "we're not allowed to modify state data anymore");
-  ok(shouldThrow(() => ss.setWindowValue(win, "foo", "baz")),
-     "we're not allowed to modify state data anymore");
-}
+  Assert.throws(() => ss.setWindowState(win, {}),
+    "we're not allowed to modify state data anymore");
+  Assert.throws(() => ss.setWindowValue(win, "foo", "baz"),
+    "we're not allowed to modify state data anymore");
+});
 
 function checkWindowState(window) {
   let {windows: [{tabs}]} = JSON.parse(ss.getWindowState(window));
   is(tabs.length, 1, "the window has a single tab");
   is(tabs[0].entries[0].url, "about:mozilla", "the tab is about:mozilla");
 
   is(ss.getClosedTabCount(window), 1, "the window has one closed tab");
   let [{state: {entries: [{url}]}}] = JSON.parse(ss.getClosedTabData(window));
@@ -66,15 +60,8 @@ function checkWindowState(window) {
 
 function shouldThrow(f) {
   try {
     f();
   } catch (e) {
     return true;
   }
 }
-
-function whenWindowClosed(window) {
-  window.addEventListener("SSWindowClosing", function onClosing() {
-    window.removeEventListener("SSWindowClosing", onClosing);
-    executeSoon(next);
-  });
-}
--- a/browser/components/sessionstore/test/browser_formdata.js
+++ b/browser/components/sessionstore/test/browser_formdata.js
@@ -68,18 +68,17 @@ add_task(function test_old_format() {
 
   // Create a tab with an iframe containing an input field.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Check that the form value is restored.
   let state = {entries: [{url: URL, formdata: {id: {input: VALUE}}}]};
-  ss.setTabState(tab, JSON.stringify(state));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, state);
   is((yield getInputValue(browser, "input")), VALUE, "form data restored");
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * This test ensures that we maintain backwards compatibility with the form
@@ -92,18 +91,17 @@ add_task(function test_old_format_inner_
 
   // Create a tab with an iframe containing an input field.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Restore the tab state.
   let state = {entries: [{url: URL, innerHTML: VALUE}]};
-  ss.setTabState(tab, JSON.stringify(state));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, state);
 
   // Check that the innerHTML value was restored.
   let html = yield getInnerHTML(browser);
   is(html, VALUE, "editable document has been restored correctly");
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
@@ -125,18 +123,17 @@ add_task(function test_url_check() {
   // Restore a tab state with a given form data url.
   function restoreStateWithURL(url) {
     let state = {entries: [{url: URL}], formdata: {id: {input: VALUE}}};
 
     if (url) {
       state.formdata.url = url;
     }
 
-    ss.setTabState(tab, JSON.stringify(state));
-    return promiseTabRestored(tab).then(() => getInputValue(browser, "input"));
+    return promiseTabState(tab, state).then(() => getInputValue(browser, "input"));
   }
 
   // Check that the form value is restored with the correct URL.
   is((yield restoreStateWithURL(URL)), VALUE, "form data restored");
 
   // Check that the form value is *not* restored with the wrong URL.
   is((yield restoreStateWithURL(URL + "?")), "", "form data not restored");
   is((yield restoreStateWithURL()), "", "form data not restored");
--- a/browser/components/sessionstore/test/browser_formdata_format.js
+++ b/browser/components/sessionstore/test/browser_formdata_format.js
@@ -70,19 +70,17 @@ function test() {
 function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
   let URL = ROOT + "browser_formdata_format_sample.html";
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   let tabState = { entries: [{ url: URL, formdata: aFormData}] };
 
   Task.spawn(function () {
     yield promiseBrowserLoaded(tab.linkedBrowser);
-
-    ss.setTabState(tab, JSON.stringify(tabState));
-    yield promiseTabRestored(tab);
+    yield promiseTabState(tab, tabState);
 
     TabState.flush(tab.linkedBrowser);
     let restoredTabState = JSON.parse(ss.getTabState(tab));
     let restoredFormData = restoredTabState.formdata;
 
     if (restoredFormData) {
       let doc = tab.linkedBrowser.contentDocument;
       let input1 = doc.getElementById("input1");
--- a/browser/components/sessionstore/test/browser_global_store.js
+++ b/browser/components/sessionstore/test/browser_global_store.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests the API for saving global session data.
-function runTests() {
+add_task(function* () {
   const key1 = "Unique name 1: " + Date.now();
   const key2 = "Unique name 2: " + Date.now();
   const value1 = "Unique value 1: " + Math.random();
   const value2 = "Unique value 2: " + Math.random();
 
   let global = {};
   global[key1] = value1;
 
@@ -34,16 +34,12 @@ function runTests() {
 
     ss.setGlobalValue(key2, value2);
     is(ss.getGlobalValue(key2), value2, "previously stored value was overwritten");
 
     ss.deleteGlobalValue(key2);
     is(ss.getGlobalValue(key2), "", "global value was deleted");
   }
 
-  yield waitForBrowserState(testState, next);
+  yield promiseBrowserState(testState);
   testRestoredState();
   testGlobalStore();
-}
-
-function test() {
-  TestRunner.run();
-}
+});
--- a/browser/components/sessionstore/test/browser_history_cap.js
+++ b/browser/components/sessionstore/test/browser_history_cap.js
@@ -42,18 +42,17 @@ add_task(function *test_history_cap() {
   // tab-state index to the middle session history entry.
   let tabState = {entries: [], index: middleEntry + 1};
   for (let i = 0; i < maxEntries; i++) {
     tabState.entries.push({url: baseURL + i});
   }
 
   info("Testing situation where only a subset of session history entries should be restored.");
 
-  ss.setTabState(tab, JSON.stringify(tabState));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, tabState);
   TabState.flush(tab.linkedBrowser);
 
   let restoredTabState = JSON.parse(ss.getTabState(tab));
   is(restoredTabState.entries.length, maxBack1 + 1 + maxFwd1,
     "The expected number of session history entries was restored.");
   is(restoredTabState.index, maxBack1 + 1, "The restored tab-state index is correct");
 
   let indexURLOffset = middleEntry - (restoredTabState.index - 1);
@@ -63,18 +62,17 @@ add_task(function *test_history_cap() {
   }
 
   // Set the relevant preferences for the other tests.
   gPrefService.setIntPref("browser.sessionstore.max_serialize_back", maxBack2);
   gPrefService.setIntPref("browser.sessionstore.max_serialize_forward", maxFwd2);
 
   info("Testing situation where all of the entries in the session history should be restored.");
 
-  ss.setTabState(tab, JSON.stringify(tabState));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, tabState);
   TabState.flush(tab.linkedBrowser);
 
   restoredTabState = JSON.parse(ss.getTabState(tab));
   is(restoredTabState.entries.length, maxEntries,
     "The expected number of session history entries was restored.");
   is(restoredTabState.index, middleEntry + 1, "The restored tab-state index is correct");
 
   for (let i = middleEntry - 2; i <= middleEntry + 2; i++) {
@@ -82,18 +80,17 @@ add_task(function *test_history_cap() {
         "URL of restored entry matches the expected URL.");
   }
 
   info("Testing situation where only the 1 + maxFwd2 oldest entries should be restored.");
 
   // Set the one-based tab-state index to the oldest session history entry.
   tabState.index = 1;
 
-  ss.setTabState(tab, JSON.stringify(tabState));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, tabState);
   TabState.flush(tab.linkedBrowser);
 
   restoredTabState = JSON.parse(ss.getTabState(tab));
   is(restoredTabState.entries.length, 1 + maxFwd2,
     "The expected number of session history entries was restored.");
   is(restoredTabState.index, 1, "The restored tab-state index is correct");
 
   for (let i = 0; i <= 2; i++) {
@@ -101,18 +98,17 @@ add_task(function *test_history_cap() {
         "URL of restored entry matches the expected URL.");
   }
 
   info("Testing situation where only the maxBack2 + 1 newest entries should be restored.");
 
   // Set the one-based tab-state index to the newest session history entry.
   tabState.index = maxEntries;
 
-  ss.setTabState(tab, JSON.stringify(tabState));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, tabState);
   TabState.flush(tab.linkedBrowser);
 
   restoredTabState = JSON.parse(ss.getTabState(tab));
   is(restoredTabState.entries.length, maxBack2 + 1,
     "The expected number of session history entries was restored.");
   is(restoredTabState.index, maxBack2 + 1, "The restored tab-state index is correct");
 
   indexURLOffset = (maxEntries - 1) - maxBack2;
--- a/browser/components/sessionstore/test/browser_privatetabs.js
+++ b/browser/components/sessionstore/test/browser_privatetabs.js
@@ -63,19 +63,17 @@ add_task(function() {
   }
 });
 
 add_task(function () {
   const FRAME_SCRIPT = "data:," +
     "docShell.QueryInterface%28Components.interfaces.nsILoadContext%29.usePrivateBrowsing%3Dtrue";
 
   // Clear the list of closed windows.
-  while (ss.getClosedWindowCount()) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
 
   // Create a new window to attach our frame script to.
   let win = yield promiseNewWindowLoaded();
   let mm = win.getGroupMessageManager("browsers");
   mm.loadFrameScript(FRAME_SCRIPT, true);
 
   // Create a new tab in the new window that will load the frame script.
   let tab = win.gBrowser.addTab("about:mozilla");
@@ -104,19 +102,17 @@ add_task(function () {
   // Check that all private tabs are removed when the non-private
   // window is closed and we don't save windows without any tabs.
   yield promiseWindowClosed(win);
   is(ss.getClosedWindowCount(), 0, "no windows to restore");
 });
 
 add_task(function () {
   // Clear the list of closed windows.
-  while (ss.getClosedWindowCount()) {
-    ss.forgetClosedWindow(0);
-  }
+  forgetClosedWindows();
 
   // Create a new window to attach our frame script to.
   let win = yield promiseNewWindowLoaded({private: true});
 
   // Create a new tab in the new window that will load the frame script.
   let tab = win.gBrowser.addTab("about:mozilla");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
--- a/browser/components/sessionstore/test/browser_scrollPositions.js
+++ b/browser/components/sessionstore/test/browser_scrollPositions.js
@@ -110,18 +110,17 @@ add_task(function test_scroll_old_format
   const TAB_STATE = { entries: [{url: URL, scroll: SCROLL_STR}] };
 
   // Add a blank tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Apply the tab state with the old format.
-  ss.setTabState(tab, JSON.stringify(TAB_STATE));
-  yield promiseTabRestored(tab);
+  yield promiseTabState(tab, TAB_STATE);
 
   // Check that the scroll positions has been applied.
   let scroll = yield sendMessage(browser, "ss-test:getScrollPosition");
   is(JSON.stringify(scroll), JSON.stringify({x: SCROLL_X, y: SCROLL_Y}),
     "scroll position has been restored correctly");
 
   // Cleanup.
   gBrowser.removeTab(tab);
--- a/browser/components/sessionstore/test/browser_sessionHistory.js
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -15,17 +15,17 @@ add_task(function test_load_start() {
   // 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);
 
   // Undo close the tab.
   tab = ss.undoCloseTab(window, 0);
   browser = tab.linkedBrowser;
-  yield promiseBrowserLoaded(browser);
+  yield promiseTabRestored(tab);
 
   // Check that the correct URL was restored.
   is(browser.currentURI.spec, "about:mozilla", "url is correct");
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
--- a/browser/components/sessionstore/test/browser_telemetry.js
+++ b/browser/components/sessionstore/test/browser_telemetry.js
@@ -19,19 +19,17 @@ function lt(a, b, message) {
 }
 function gt(a, b, message) {
   isnot(a, undefined, message + " (sanity check)");
   isnot(b, undefined, message + " (sanity check)");
   ok(a > b, message + " ( " + a + " > " + b + ")");
 }
 
 add_task(function init() {
-  for (let i = ss.getClosedWindowCount() - 1; i >= 0; --i) {
-    ss.forgetClosedWindow(i);
-  }
+  forgetClosedWindows();
   for (let i = ss.getClosedTabCount(window) - 1; i >= 0; --i) {
     ss.forgetClosedTab(window, i);
   }
 });
 
 /**
  * Test that Telemetry collection doesn't cause any error.
  */
--- a/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js
@@ -3,18 +3,17 @@
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // This test checks that closed private windows can't be restored
 
 function test() {
   waitForExplicitFinish();
 
   // Purging the list of closed windows
-  while(ss.getClosedWindowCount() > 0)
-    ss.forgetClosedWindow(0);
+  forgetClosedWindows();
 
   // Load a private window, then close it 
   // and verify it doesn't get remembered for restoring
   whenNewWindowLoaded({private: true}, function (win) {
     info("The private window got loaded");
     win.addEventListener("SSWindowClosing", function onclosing() {
       win.removeEventListener("SSWindowClosing", onclosing, false);
       executeSoon(function () {
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -167,39 +167,24 @@ function waitForBrowserState(aState, aSe
   // Finally, call setBrowserState
   ss.setBrowserState(JSON.stringify(aState));
 }
 
 function promiseBrowserState(aState) {
   return new Promise(resolve => waitForBrowserState(aState, resolve));
 }
 
-// Doesn't assume that the tab needs to be closed in a cleanup function.
-// If that's the case, the test author should handle that in the test.
-function waitForTabState(aTab, aState, aCallback) {
-  let listening = true;
-
-  function onSSTabRestored() {
-    aTab.removeEventListener("SSTabRestored", onSSTabRestored, false);
-    listening = false;
-    aCallback();
+function promiseTabState(tab, state) {
+  if (typeof(state) != "string") {
+    state = JSON.stringify(state);
   }
 
-  aTab.addEventListener("SSTabRestored", onSSTabRestored, false);
-
-  registerCleanupFunction(function() {
-    if (listening) {
-      aTab.removeEventListener("SSTabRestored", onSSTabRestored, false);
-    }
-  });
-  ss.setTabState(aTab, JSON.stringify(aState));
-}
-
-function promiseTabState(tab, state) {
-  return new Promise(resolve => waitForTabState(tab, state, resolve));
+  let promise = promiseTabRestored(tab);
+  ss.setTabState(tab, state);
+  return promise;
 }
 
 /**
  * Wait for a content -> chrome message.
  */
 function promiseContentMessage(browser, name) {
   let mm = browser.messageManager;
 
@@ -292,89 +277,39 @@ let promiseForEachSessionRestoreFile = T
     } catch (ex if ex instanceof OS.File.Error
 	     && ex.becauseNoSuchFile) {
       // Ignore missing files
     }
     cb(data, key);
   }
 });
 
-function whenBrowserLoaded(aBrowser, aCallback = next, ignoreSubFrames = true, expectedURL = null) {
-  aBrowser.messageManager.addMessageListener("ss-test:loadEvent", function onLoad(msg) {
-    if (expectedURL && aBrowser.currentURI.spec != expectedURL) {
-      return;
-    }
-
-    if (!ignoreSubFrames || !msg.data.subframe) {
-      aBrowser.messageManager.removeMessageListener("ss-test:loadEvent", onLoad);
-      executeSoon(aCallback);
-    }
-  });
-}
 function promiseBrowserLoaded(aBrowser, ignoreSubFrames = true) {
   return new Promise(resolve => {
-    whenBrowserLoaded(aBrowser, resolve, ignoreSubFrames);
-  });
-}
-function whenBrowserUnloaded(aBrowser, aContainer, aCallback = next) {
-  aBrowser.addEventListener("unload", function onUnload() {
-    aBrowser.removeEventListener("unload", onUnload, true);
-    executeSoon(aCallback);
-  }, true);
-}
-
-/**
- * Loads a page in a browser, and returns a Promise that
- * resolves once a "load" event has been fired within that
- * browser.
- *
- * @param browser
- *        The browser to load the page in.
- * @param uri
- *        The URI to load.
- *
- * @return Promise
- */
-function loadPage(browser, uri) {
-  return new Promise((resolve, reject) => {
-    browser.addEventListener("load", function onLoad(event) {
-      browser.removeEventListener("load", onLoad, true);
-      resolve();
-    }, true);
-    browser.loadURI(uri);
-  });
-}
-
-function promiseBrowserUnloaded(aBrowser, aContainer) {
-  return new Promise(resolve => {
-    whenBrowserUnloaded(aBrowser, aContainer, resolve);
+    aBrowser.messageManager.addMessageListener("ss-test:loadEvent", function onLoad(msg) {
+      if (!ignoreSubFrames || !msg.data.subframe) {
+        aBrowser.messageManager.removeMessageListener("ss-test:loadEvent", onLoad);
+        resolve();
+      }
+    });
   });
 }
 
 function whenWindowLoaded(aWindow, aCallback = next) {
   aWindow.addEventListener("load", function windowLoadListener() {
     aWindow.removeEventListener("load", windowLoadListener, false);
     executeSoon(function executeWhenWindowLoaded() {
       aCallback(aWindow);
     });
   }, false);
 }
 function promiseWindowLoaded(aWindow) {
   return new Promise(resolve => whenWindowLoaded(aWindow, resolve));
 }
 
-function whenTabRestored(aTab, aCallback = next) {
-  aTab.addEventListener("SSTabRestored", function onRestored(aEvent) {
-    aTab.removeEventListener("SSTabRestored", onRestored, true);
-    executeSoon(function executeWhenTabRestored() {
-      aCallback();
-    });
-  }, true);
-}
-
 var gUniqueCounter = 0;
 function r() {
   return Date.now() + "-" + (++gUniqueCounter);
 }
 
 function BrowserWindowIterator() {
   let windowsEnum = Services.wm.getEnumerator("navigator:browser");
   while (windowsEnum.hasMoreElements()) {
@@ -459,24 +394,33 @@ let gProgressListener = {
     return [needsRestore, isRestoring, wasRestored];
   }
 };
 
 registerCleanupFunction(function () {
   gProgressListener.unsetCallback();
 });
 
-// Close everything but our primary window. We can't use waitForFocus()
-// because apparently it's buggy. See bug 599253.
-function closeAllButPrimaryWindow() {
+// Close all but our primary window.
+function promiseAllButPrimaryWindowClosed() {
+  let windows = [];
   for (let win in BrowserWindowIterator()) {
     if (win != window) {
-      win.close();
+      windows.push(win);
     }
   }
+
+  return Promise.all(windows.map(promiseWindowClosed));
+}
+
+// Forget all closed windows.
+function forgetClosedWindows() {
+  while (ss.getClosedWindowCount() > 0) {
+    ss.forgetClosedWindow(0);
+  }
 }
 
 /**
  * When opening a new window it is not sufficient to wait for its load event.
  * We need to use whenDelayedStartupFinshed() here as the browser window's
  * delayedStartup() routine is executed one tick after the window's load event
  * has been dispatched. browser-delayed-startup-finished might be deferred even
  * further if parts of the window's initialization process take more time than
@@ -539,66 +483,16 @@ function whenDelayedStartupFinished(aWin
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
 function promiseDelayedStartupFinished(aWindow) {
   return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve));
 }
 
-/**
- * The test runner that controls the execution flow of our tests.
- */
-let TestRunner = {
-  _iter: null,
-
-  /**
-   * Holds the browser state from before we started so
-   * that we can restore it after all tests ran.
-   */
-  backupState: {},
-
-  /**
-   * Starts the test runner.
-   */
-  run: function () {
-    waitForExplicitFinish();
-
-    SessionStore.promiseInitialized.then(() => {
-      this.backupState = JSON.parse(ss.getBrowserState());
-      this._iter = runTests();
-      this.next();
-    });
-  },
-
-  /**
-   * Runs the next available test or finishes if there's no test left.
-   */
-  next: function () {
-    try {
-      TestRunner._iter.next();
-    } catch (e if e instanceof StopIteration) {
-      TestRunner.finish();
-    }
-  },
-
-  /**
-   * Finishes all tests and cleans up.
-   */
-  finish: function () {
-    closeAllButPrimaryWindow();
-    gBrowser.selectedTab = gBrowser.tabs[0];
-    waitForBrowserState(this.backupState, finish);
-  }
-};
-
-function next() {
-  TestRunner.next();
-}
-
 function promiseTabRestored(tab) {
   return new Promise(resolve => {
     tab.addEventListener("SSTabRestored", function onRestored() {
       tab.removeEventListener("SSTabRestored", onRestored);
       resolve();
     });
   });
 }
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -1554,43 +1554,48 @@ this.UITour = {
       owner: aWindow.gBrowser.selectedTab,
       relatedToCurrent: true
     });
     aWindow.gBrowser.selectedTab = tab;
   },
 
   getConfiguration: function(aMessageManager, aWindow, aConfiguration, aCallbackID) {
     switch (aConfiguration) {
-      case "availableTargets":
-        this.getAvailableTargets(aMessageManager, aWindow, aCallbackID);
-        break;
-      case "sync":
-        this.sendPageCallback(aMessageManager, aCallbackID, {
-          setup: Services.prefs.prefHasUserValue("services.sync.username"),
-        });
-        break;
       case "appinfo":
         let props = ["defaultUpdateChannel", "version"];
         let appinfo = {};
         props.forEach(property => appinfo[property] = Services.appinfo[property]);
         this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
         break;
+      case "availableTargets":
+        this.getAvailableTargets(aMessageManager, aWindow, aCallbackID);
+        break;
+      case "loop":
+        this.sendPageCallback(aMessageManager, aCallbackID, {
+          gettingStartedSeen: Services.prefs.getBoolPref("loop.gettingStarted.seen"),
+        });
+        break;
       case "selectedSearchEngine":
         Services.search.init(rv => {
           let engine;
           if (Components.isSuccessCode(rv)) {
             engine = Services.search.defaultEngine;
           } else {
             engine = { identifier: "" };
           }
           this.sendPageCallback(aMessageManager, aCallbackID, {
             searchEngineIdentifier: engine.identifier
           });
         });
         break;
+      case "sync":
+        this.sendPageCallback(aMessageManager, aCallbackID, {
+          setup: Services.prefs.prefHasUserValue("services.sync.username"),
+        });
+        break;
       default:
         log.error("getConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
   setConfiguration: function(aConfiguration, aValue) {
     switch (aConfiguration) {
--- a/browser/components/uitour/test/browser_UITour_loop.js
+++ b/browser/components/uitour/test/browser_UITour_loop.js
@@ -55,16 +55,24 @@ let tests = [
     gContentAPI.showMenu("loop");
     gContentAPI.getConfiguration("availableTargets", (data) => {
       for (let targetName of ["loop-newRoom", "loop-roomList", "loop-signInUpLink"]) {
         isnot(data.targets.indexOf(targetName), -1, targetName + " should exist");
       }
       done();
     });
   },
+  function test_getConfigurationLoop(done) {
+    let gettingStartedSeen = Services.prefs.getBoolPref("loop.gettingStarted.seen");
+    gContentAPI.getConfiguration("loop", (data) => {
+      is(data.gettingStartedSeen, gettingStartedSeen,
+         "The configuration property should equal that of the pref");
+      done();
+    });
+  },
   function test_hideMenuHidesAnnotations(done) {
     let infoPanel = document.getElementById("UITourTooltip");
     let highlightPanel = document.getElementById("UITourHighlightContainer");
 
     gContentAPI.showMenu("loop", function menuCallback() {
       gContentAPI.showHighlight("loop-roomList");
       gContentAPI.showInfo("loop-newRoom", "Make a new room", "AKA. conversation");
       UITour.getTarget(window, "loop-newRoom").then((target) => {
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -93,16 +93,17 @@ browser.jar:
     content/browser/devtools/ui-profile.js                             (profiler/ui-profile.js)
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
     content/browser/devtools/performance.xul                           (performance/performance.xul)
     content/browser/devtools/performance/performance-controller.js     (performance/performance-controller.js)
     content/browser/devtools/performance/performance-view.js           (performance/performance-view.js)
     content/browser/devtools/performance/views/overview.js             (performance/views/overview.js)
     content/browser/devtools/performance/views/toolbar.js              (performance/views/toolbar.js)
     content/browser/devtools/performance/views/details.js              (performance/views/details.js)
+    content/browser/devtools/performance/views/details-subview.js      (performance/views/details-abstract-subview.js)
     content/browser/devtools/performance/views/details-call-tree.js    (performance/views/details-call-tree.js)
     content/browser/devtools/performance/views/details-waterfall.js    (performance/views/details-waterfall.js)
     content/browser/devtools/performance/views/details-flamegraph.js   (performance/views/details-flamegraph.js)
     content/browser/devtools/performance/views/recordings.js           (performance/views/recordings.js)
 #endif
     content/browser/devtools/responsivedesign/resize-commands.js       (responsivedesign/resize-commands.js)
     content/browser/devtools/commandline.css                           (commandline/commandline.css)
     content/browser/devtools/commandlineoutput.xhtml                   (commandline/commandlineoutput.xhtml)
--- a/browser/devtools/performance/modules/recording-utils.js
+++ b/browser/devtools/performance/modules/recording-utils.js
@@ -52,8 +52,48 @@ exports.RecordingUtils.offsetSampleTimes
  *        The amount of time to offset by (in milliseconds).
  */
 exports.RecordingUtils.offsetMarkerTimes = function(markers, timeOffset) {
   for (let marker of markers) {
     marker.start -= timeOffset;
     marker.end -= timeOffset;
   }
 }
+
+/**
+ * Converts allocation data from the memory actor to something that follows
+ * the same structure as the samples data received from the profiler.
+ *
+ * @see MemoryActor.prototype.getAllocations for more information.
+ *
+ * @param object allocations
+ *        A list of { sites, timestamps, frames, counts } arrays.
+ * @return array
+ *         The samples data.
+ */
+exports.RecordingUtils.getSamplesFromAllocations = function(allocations) {
+  let { sites, timestamps, frames, counts } = allocations;
+  let samples = [];
+
+  for (let i = 0, len = sites.length; i < len; i++) {
+    let site = sites[i];
+    let timestamp = timestamps[i];
+    let frame = frames[site];
+    let count = counts[site];
+
+    let sample = { time: timestamp, frames: [] };
+    samples.push(sample);
+
+    while (frame) {
+      sample.frames.push({
+        location: frame.source + ":" + frame.line + ":" + frame.column,
+        allocations: count
+      });
+      site = frame.parent;
+      frame = frames[site];
+      count = counts[site];
+    }
+
+    sample.frames.reverse();
+  }
+
+  return samples;
+}
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -14,16 +14,17 @@
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="application/javascript" src="performance/performance-controller.js"/>
   <script type="application/javascript" src="performance/performance-view.js"/>
   <script type="application/javascript" src="performance/recording-model.js"/>
   <script type="application/javascript" src="performance/views/overview.js"/>
   <script type="application/javascript" src="performance/views/toolbar.js"/>
+  <script type="application/javascript" src="performance/views/details-subview.js"/>
   <script type="application/javascript" src="performance/views/details-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-waterfall.js"/>
   <script type="application/javascript" src="performance/views/details-flamegraph.js"/>
   <script type="application/javascript" src="performance/views/details.js"/>
   <script type="application/javascript" src="performance/views/recordings.js"/>
 
   <popupset id="performance-options-popupset">
     <menupopup id="performance-options-menupopup">
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -4,16 +4,17 @@ subsuite = devtools
 support-files =
   doc_simple-test.html
   head.js
 
 # Commented out tests are profiler tests
 # that need to be moved over to performance tool
 
 [browser_perf-aaa-run-first-leaktest.js]
+[browser_perf-allocations-to-samples.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-data-samples.js]
 [browser_perf-details-calltree-render.js]
 [browser_perf-details-flamegraph-render.js]
 [browser_perf-details-waterfall-render.js]
 [browser_perf-details-01.js]
 [browser_perf-details-02.js]
 [browser_perf-front-basic-profiler-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-allocations-to-samples.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if allocations data received from the memory actor is properly
+ * converted to something that follows the same structure as the samples data
+ * received from the profiler.
+ */
+
+function test() {
+  let { RecordingUtils } = devtools.require("devtools/performance/recording-utils");
+
+  let output = RecordingUtils.getSamplesFromAllocations(TEST_DATA);
+  is(output.toSource(), EXPECTED_OUTPUT.toSource(), "The output is correct.");
+
+  finish();
+}
+
+let TEST_DATA = {
+  sites: [0, 0, 1, 2, 3],
+  timestamps: [50, 100, 150, 200, 250],
+  frames: [
+    null,
+    {
+      source: "A",
+      line: 1,
+      column: 2,
+      parent: 0
+    },
+    {
+      source: "B",
+      line: 3,
+      column: 4,
+      parent: 1
+    },
+    {
+      source: "C",
+      line: 5,
+      column: 6,
+      parent: 2
+    }
+  ],
+  counts: [11, 22, 33, 44]
+};
+
+let EXPECTED_OUTPUT = [{
+  time: 50,
+  frames: []
+}, {
+  time: 100,
+  frames: []
+}, {
+  time: 150,
+  frames: [{
+    location: "A:1:2",
+    allocations: 22
+  }]
+}, {
+  time: 200,
+  frames: [{
+    location: "A:1:2",
+    allocations: 22
+  }, {
+    location: "B:3:4",
+    allocations: 33
+  }]
+}, {
+  time: 250,
+  frames: [{
+    location: "A:1:2",
+    allocations: 22
+  }, {
+    location: "B:3:4",
+    allocations: 33
+  }, {
+    location: "C:5:6",
+    allocations: 44
+  }]
+}];
--- a/browser/devtools/performance/test/browser_perf-details-01.js
+++ b/browser/devtools/performance/test/browser_perf-details-01.js
@@ -32,20 +32,20 @@ function spawnTest () {
   is(viewName, "flamegraph", "DETAILS_VIEW_SELECTED fired with view name");
   checkViews(DetailsView, doc, "flamegraph");
 
   yield teardown(panel);
   finish();
 }
 
 function checkViews (DetailsView, doc, currentView) {
-  for (let viewName in DetailsView.viewIndexes) {
+  for (let viewName in DetailsView.components) {
     let button = doc.querySelector(`toolbarbutton[data-view="${viewName}"]`);
 
-    is(DetailsView.el.selectedIndex, DetailsView.viewIndexes[currentView],
+    is(DetailsView.el.selectedPanel.id, DetailsView.components[currentView].id,
       `DetailsView correctly has ${currentView} selected.`);
     if (viewName === currentView) {
       ok(button.getAttribute("checked"), `${viewName} button checked`);
     } else {
       ok(!button.getAttribute("checked"), `${viewName} button not checked`);
     }
   }
 }
--- a/browser/devtools/performance/test/browser_perf-details-calltree-render.js
+++ b/browser/devtools/performance/test/browser_perf-details-calltree-render.js
@@ -1,17 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that the call tree view renders content after recording.
  */
 function spawnTest () {
   let { panel } = yield initPerformance(SIMPLE_URL);
-  let { EVENTS, CallTreeView } = panel.panelWin;
+  let { EVENTS, DetailsView, CallTreeView } = panel.panelWin;
+
+  DetailsView.selectView("calltree");
+  ok(DetailsView.isViewSelected(CallTreeView), "The call tree is now selected.");
 
   yield startRecording(panel);
   yield busyWait(100);
 
   let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
   yield stopRecording(panel);
   yield rendered;
 
--- a/browser/devtools/performance/test/browser_perf-details-flamegraph-render.js
+++ b/browser/devtools/performance/test/browser_perf-details-flamegraph-render.js
@@ -1,17 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that the flamegraph view renders content after recording.
  */
 function spawnTest () {
   let { panel } = yield initPerformance(SIMPLE_URL);
-  let { EVENTS, FlameGraphView } = panel.panelWin;
+  let { EVENTS, DetailsView, FlameGraphView } = panel.panelWin;
+
+  DetailsView.selectView("flamegraph");
+  ok(DetailsView.isViewSelected(FlameGraphView), "The flamegraph is now selected.");
 
   yield startRecording(panel);
   yield busyWait(100);
 
   let rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
   yield stopRecording(panel);
   yield rendered;
 
--- a/browser/devtools/performance/test/browser_perf-details-waterfall-render.js
+++ b/browser/devtools/performance/test/browser_perf-details-waterfall-render.js
@@ -1,17 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that the waterfall view renders content after recording.
  */
 function spawnTest () {
   let { panel } = yield initPerformance(SIMPLE_URL);
-  let { EVENTS, PerformanceController, WaterfallView } = panel.panelWin;
+  let { EVENTS, PerformanceController, DetailsView, WaterfallView } = panel.panelWin;
+
+  ok(DetailsView.isViewSelected(WaterfallView),
+    "The waterfall view is selected by default in the details view.");
 
   yield startRecording(panel);
   yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
 
   let rendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
   yield stopRecording(panel);
   yield rendered;
 
--- a/browser/devtools/performance/test/browser_perf-options-invert-call-tree.js
+++ b/browser/devtools/performance/test/browser_perf-options-invert-call-tree.js
@@ -3,20 +3,23 @@
 
 const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
 
 /**
  * Tests that the call tree view renders after recording.
  */
 function spawnTest () {
   let { panel } = yield initPerformance(SIMPLE_URL);
-  let { EVENTS, CallTreeView } = panel.panelWin;
+  let { EVENTS, DetailsView, CallTreeView } = panel.panelWin;
 
   Services.prefs.setBoolPref(INVERT_PREF, true);
 
+  DetailsView.selectView("calltree");
+  ok(DetailsView.isViewSelected(CallTreeView), "The call tree is now selected.");
+
   yield startRecording(panel);
   yield busyWait(100);
 
   let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
   yield stopRecording(panel);
   yield rendered;
 
   rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
--- a/browser/devtools/performance/test/browser_perf-range-changed-render.js
+++ b/browser/devtools/performance/test/browser_perf-range-changed-render.js
@@ -47,14 +47,14 @@ function spawnTest () {
   ok(true, "Call tree rerenders after its corresponding pane is shown.");
 
   rendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
   DetailsView.selectView("waterfall");
   yield rendered;
   ok(true, "Waterfall rerenders after its corresponding pane is shown.");
 
   is(updatedWaterfall, 3, "WaterfallView rerendered 3 times.");
-  is(updatedCallTree, 3, "CallTreeView rerendered 3 times.");
-  is(updatedFlameGraph, 3, "FlameGraphView rerendered 3 times.");
+  is(updatedCallTree, 2, "CallTreeView rerendered 2 times.");
+  is(updatedFlameGraph, 2, "FlameGraphView rerendered 2 times.");
 
   yield teardown(panel);
   finish();
 }
--- a/browser/devtools/performance/test/browser_perf-recording-selected-04.js
+++ b/browser/devtools/performance/test/browser_perf-recording-selected-04.js
@@ -1,19 +1,21 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests that all components get rerendered for a profile when switching.
+ * Tests that all components can get rerendered for a profile when switching.
  */
 
 let test = Task.async(function*() {
   let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
-  let { $, EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { $, EVENTS, PerformanceController, DetailsSubview, RecordingsView } = panel.panelWin;
+
+  DetailsSubview.canUpdateWhileHidden = true;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   let rerender = waitForWidgetsRendered(panel);
--- a/browser/devtools/performance/test/browser_perf_recordings-io-01.js
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-01.js
@@ -2,17 +2,19 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the performance tool is able to save and load recordings.
  */
 
 let test = Task.async(function*() {
   let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
-  let { EVENTS, PerformanceController } = panel.panelWin;
+  let { EVENTS, PerformanceController, DetailsSubview } = panel.panelWin;
+
+  DetailsSubview.canUpdateWhileHidden = true;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   // Verify original recording.
 
   let originalData = PerformanceController.getCurrentRecording().getAllData();
   ok(originalData, "The original recording is not empty.");
--- a/browser/devtools/performance/test/browser_perf_recordings-io-04.js
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-04.js
@@ -3,17 +3,19 @@
 
 /**
  * Tests if the performance tool can import profiler data from the
  * original profiler tool.
  */
 
 let test = Task.async(function*() {
   let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
-  let { EVENTS, PerformanceController } = panel.panelWin;
+  let { EVENTS, PerformanceController, DetailsSubview } = panel.panelWin;
+
+  DetailsSubview.canUpdateWhileHidden = true;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   // Get data from the current profiler
   let data = PerformanceController.getCurrentRecording().getAllData();
 
   // Create a structure from the data that mimics the old profiler's data.
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/views/details-abstract-subview.js
@@ -0,0 +1,97 @@
+/* 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";
+
+/**
+ * A base class from which all detail views inherit.
+ */
+let DetailsSubview = {
+  /**
+   * Sets up the view with event binding.
+   */
+  initialize: function () {
+    this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
+    this._onOverviewRangeChange = this._onOverviewRangeChange.bind(this);
+    this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+
+    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
+    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
+    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onOverviewRangeChange);
+    DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+  },
+
+  /**
+   * Unbinds events.
+   */
+  destroy: function () {
+    clearNamedTimeout("range-change-debounce");
+
+    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
+    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
+    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onOverviewRangeChange);
+    DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+  },
+
+  /**
+   * Amount of time (in milliseconds) to wait until this view gets updated,
+   * when the range is changed in the overview.
+   */
+  rangeChangeDebounceTime: 0,
+
+  /**
+   * Flag specifying if this view should be updated when selected. This will
+   * be set to true, for example, when the range changes in the overview and
+   * this view is not currently visible.
+   */
+  shouldUpdateWhenShown: false,
+
+  /**
+   * Flag specifying if this view may get updated even when it's not selected.
+   * Should only be used in tests.
+   */
+  canUpdateWhileHidden: false,
+
+  /**
+   * Called when recording stops or is selected.
+   */
+  _onRecordingStoppedOrSelected: function(_, recording) {
+    if (recording.isRecording()) {
+      return;
+    }
+    if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+      this.render();
+    } else {
+      this.shouldUpdateWhenShown = true;
+    }
+  },
+
+  /**
+   * Fired when a range is selected or cleared in the OverviewView.
+   */
+  _onOverviewRangeChange: function (_, interval) {
+    if (DetailsView.isViewSelected(this)) {
+      let debounced = () => this.render(interval);
+      setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, debounced);
+    } else {
+      this.shouldUpdateWhenShown = true;
+    }
+  },
+
+  /**
+   * Fired when a view is selected in the DetailsView.
+   */
+  _onDetailsViewSelected: function() {
+    if (DetailsView.isViewSelected(this) && this.shouldUpdateWhenShown) {
+      this.render(OverviewView.getTimeInterval());
+      this.shouldUpdateWhenShown = false;
+    }
+  }
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(DetailsSubview);
--- a/browser/devtools/performance/views/details-call-tree.js
+++ b/browser/devtools/performance/views/details-call-tree.js
@@ -1,49 +1,38 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const CALLTREE_UPDATE_DEBOUNCE = 50; // ms
-
 /**
  * CallTree view containing profiler call tree, controlled by DetailsView.
  */
-let CallTreeView = {
+let CallTreeView = Heritage.extend(DetailsSubview, {
+  rangeChangeDebounceTime: 50, // ms
+
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
-    this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
-    this._onRangeChange = this._onRangeChange.bind(this);
-    this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+    DetailsSubview.initialize.call(this);
+
     this._onPrefChanged = this._onPrefChanged.bind(this);
     this._onLink = this._onLink.bind(this);
 
-    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
-    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
     PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
-    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
-    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
-    DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
-    clearNamedTimeout("calltree-update");
+    DetailsSubview.destroy.call(this);
 
-    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
-    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
     PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
-    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
-    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
-    DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
   },
 
   /**
    * Method for handling all the set up for rendering a new call tree.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    * @param object options [optional]
@@ -53,48 +42,16 @@ let CallTreeView = {
     let recording = PerformanceController.getCurrentRecording();
     let profile = recording.getProfile();
     let threadNode = this._prepareCallTree(profile, interval, options);
     this._populateCallTree(threadNode, options);
     this.emit(EVENTS.CALL_TREE_RENDERED);
   },
 
   /**
-   * Called when recording is stopped or has been selected.
-   */
-  _onRecordingStoppedOrSelected: function (_, recording) {
-    if (!recording.isRecording()) {
-      this.render();
-    }
-  },
-
-  /**
-   * Fired when a range is selected or cleared in the OverviewView.
-   */
-  _onRangeChange: function (_, interval) {
-    if (DetailsView.isViewSelected(this)) {
-      let debounced = () => this.render(interval);
-      setNamedTimeout("calltree-update", CALLTREE_UPDATE_DEBOUNCE, debounced);
-    } else {
-      this._dirty = true;
-      this._interval = interval;
-    }
-  },
-
-  /**
-   * Fired when a view is selected in the DetailsView.
-   */
-  _onDetailsViewSelected: function() {
-    if (DetailsView.isViewSelected(this) && this._dirty) {
-      this.render(this._interval);
-      this._dirty = false;
-    }
-  },
-
-  /**
    * Fired on the "link" event for the call tree in this container.
    */
   _onLink: function (_, treeItem) {
     let { url, line } = treeItem.frame.getInfo();
     viewSourceInDebugger(url, line).then(
       () => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER),
       () => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER));
   },
@@ -150,22 +107,17 @@ let CallTreeView = {
   /**
    * Called when a preference under "devtools.performance.ui." is changed.
    */
   _onPrefChanged: function (_, prefName, value) {
     if (prefName === "invert-call-tree") {
       this.render(OverviewView.getTimeInterval());
     }
   }
-};
-
-/**
- * Convenient way of emitting events from the view.
- */
-EventEmitter.decorate(CallTreeView);
+});
 
 /**
  * Opens/selects the debugger in this toolbox and jumps to the specified
  * file name and line number.
  * @param string url
  * @param number line
  */
 let viewSourceInDebugger = Task.async(function *(url, line) {
--- a/browser/devtools/performance/views/details-flamegraph.js
+++ b/browser/devtools/performance/views/details-flamegraph.js
@@ -2,113 +2,73 @@
  * 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";
 
 /**
  * FlameGraph view containing a pyramid-like visualization of a profile,
  * controlled by DetailsView.
  */
-let FlameGraphView = {
+let FlameGraphView = Heritage.extend(DetailsSubview, {
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function* () {
-    this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
-    this._onRangeChange = this._onRangeChange.bind(this);
-    this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
-    this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+    DetailsSubview.initialize.call(this);
 
     this.graph = new FlameGraph($("#flamegraph-view"));
     this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
     yield this.graph.ready();
 
-    this.graph.on("selecting", this._onRangeChangeInGraph);
+    this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
 
-    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
-    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
-    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
-    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
-    DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+    this.graph.on("selecting", this._onRangeChangeInGraph);
   }),
 
   /**
    * Unbinds events.
    */
   destroy: function () {
-    this.graph.off("selecting", this._onRangeChangeInGraph);
+    DetailsSubview.destroy.call(this);
 
-    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
-    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
-    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
-    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
-    DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
+    this.graph.off("selecting", this._onRangeChangeInGraph);
   },
 
   /**
    * Method for handling all the set up for rendering a new flamegraph.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
   render: function (interval={}) {
     let recording = PerformanceController.getCurrentRecording();
-    let startTime = interval.startTime || 0;
-    let endTime = interval.endTime || recording.getDuration();
-    this.graph.setViewRange({ startTime, endTime });
-    this.emit(EVENTS.FLAMEGRAPH_RENDERED);
-  },
-
-  /**
-   * Called when recording is stopped or selected.
-   */
-  _onRecordingStoppedOrSelected: function (_, recording) {
-    if (recording.isRecording()) {
-      return;
-    }
+    let duration = recording.getDuration();
     let profile = recording.getProfile();
     let samples = profile.threads[0].samples;
+
     let data = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
       flattenRecursion: Prefs.flattenTreeRecursion,
       filterFrames: !Prefs.showPlatformData && FrameNode.isContent,
       showIdleBlocks: Prefs.showIdleBlocks && L10N.getStr("table.idle")
     });
-    let startTime = 0;
-    let endTime = recording.getDuration();
-    this.graph.setData({ data, bounds: { startTime, endTime } });
-    this.render();
-  },
 
-  /**
-   * Fired when a range is selected or cleared in the OverviewView.
-   */
-  _onRangeChange: function (_, interval) {
-    if (DetailsView.isViewSelected(this)) {
-      this.render(interval);
-    } else {
-      this._dirty = true;
-      this._interval = interval;
-    }
+    this.graph.setData({ data,
+      bounds: {
+        startTime: 0,
+        endTime: duration
+      },
+      visible: {
+        startTime: interval.startTime || 0,
+        endTime: interval.endTime || duration
+      }
+    });
+
+    this.emit(EVENTS.FLAMEGRAPH_RENDERED);
   },
 
   /**
    * Fired when a range is selected or cleared in the FlameGraph.
    */
   _onRangeChangeInGraph: function () {
     let interval = this.graph.getViewRange();
     OverviewView.setTimeInterval(interval, { stopPropagation: true });
-  },
-
-  /**
-   * Fired when a view is selected in the DetailsView.
-   */
-  _onDetailsViewSelected: function() {
-    if (DetailsView.isViewSelected(this) && this._dirty) {
-      this.render(this._interval);
-      this._dirty = false;
-    }
   }
-};
-
-/**
- * Convenient way of emitting events from the view.
- */
-EventEmitter.decorate(FlameGraphView);
+});
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -1,63 +1,52 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const WATERFALL_UPDATE_DEBOUNCE = 10; // ms
-
 /**
  * Waterfall view containing the timeline markers, controlled by DetailsView.
  */
-let WaterfallView = {
+let WaterfallView = Heritage.extend(DetailsSubview, {
+  rangeChangeDebounceTime: 10, // ms
+
   /**
    * Sets up the view with event binding.
    */
-  initialize: Task.async(function *() {
+  initialize: function () {
+    DetailsSubview.initialize.call(this);
+
+    this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#details-pane"), TIMELINE_BLUEPRINT);
+    this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
+
     this._onRecordingStarted = this._onRecordingStarted.bind(this);
-    this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this);
-    this._onRangeChange = this._onRangeChange.bind(this);
-    this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
 
-    this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#details-pane"), TIMELINE_BLUEPRINT);
-    this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
+    PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
 
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
     this.details.on("resize", this._onResize);
 
-    PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
-    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
-    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
-    OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
-    OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
-    DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
-
     this.waterfall.recalculateBounds();
-  }),
+  },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
-    clearNamedTimeout("waterfall-update");
+    DetailsSubview.destroy.call(this);
+
+    PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
 
     this.waterfall.off("selected", this._onMarkerSelected);
     this.waterfall.off("unselected", this._onMarkerSelected);
     this.details.off("resize", this._onResize);
-
-    PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
-    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStoppedOrSelected);
-    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
-    OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
-    OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
-    DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
   },
 
   /**
    * Method for handling all the set up for rendering a new waterfall.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
@@ -73,48 +62,16 @@ let WaterfallView = {
   /**
    * Called when recording starts.
    */
   _onRecordingStarted: function () {
     this.waterfall.clearView();
   },
 
   /**
-   * Called when recording stops or is selected.
-   */
-  _onRecordingStoppedOrSelected: function (_, recording) {
-    if (!recording.isRecording()) {
-      this.render();
-    }
-  },
-
-  /**
-   * Fired when a range is selected or cleared in the OverviewView.
-   */
-  _onRangeChange: function (_, interval) {
-    if (DetailsView.isViewSelected(this)) {
-      let debounced = () => this.render(interval);
-      setNamedTimeout("waterfall-update", WATERFALL_UPDATE_DEBOUNCE, debounced);
-    } else {
-      this._dirty = true;
-      this._interval = interval;
-    }
-  },
-
-  /**
-   * Fired when a view is selected in the DetailsView.
-   */
-  _onDetailsViewSelected: function() {
-    if (DetailsView.isViewSelected(this) && this._dirty) {
-      this.render(this._interval);
-      this._dirty = false;
-    }
-  },
-
-  /**
    * Called when a marker is selected in the waterfall view,
    * updating the markers detail view.
    */
   _onMarkerSelected: function (event, marker) {
     let recording = PerformanceController.getCurrentRecording();
     let frames = recording.getFrames();
 
     if (event === "selected") {
@@ -127,14 +84,9 @@ let WaterfallView = {
 
   /**
    * Called when the marker details view is resized.
    */
   _onResize: function () {
     this.waterfall.recalculateBounds();
     this.render();
   }
-};
-
-/**
- * Convenient way of emitting events from the view.
- */
-EventEmitter.decorate(WaterfallView);
+});
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -6,66 +6,66 @@
 const DEFAULT_DETAILS_SUBVIEW = "waterfall";
 
 /**
  * Details view containing profiler call tree and markers waterfall. Manages
  * subviews and toggles visibility between them.
  */
 let DetailsView = {
   /**
-   * Name to index mapping of subviews, used by selecting view.
+   * Name to node+object mapping of subviews.
    */
   components: {
-    waterfall: { index: 0, view: WaterfallView },
-    calltree: { index: 1, view: CallTreeView },
-    flamegraph: { index: 2, view: FlameGraphView }
+    waterfall: { id: "waterfall-view", view: WaterfallView },
+    calltree: { id: "calltree-view", view: CallTreeView },
+    flamegraph: { id: "flamegraph-view", view: FlameGraphView }
   },
 
   /**
    * Sets up the view with event binding, initializes subviews.
    */
   initialize: Task.async(function *() {
     this.el = $("#details-pane");
     this.toolbar = $("#performance-toolbar-controls-detail-views");
 
     this._onViewToggle = this._onViewToggle.bind(this);
 
     for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
       button.addEventListener("command", this._onViewToggle);
     }
 
-    yield WaterfallView.initialize();
-    yield CallTreeView.initialize();
-    yield FlameGraphView.initialize();
+    for (let [_, { view }] of Iterator(this.components)) {
+      yield view.initialize();
+    }
 
     this.selectView(DEFAULT_DETAILS_SUBVIEW);
   }),
 
   /**
    * Unbinds events, destroys subviews.
    */
   destroy: Task.async(function *() {
     for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
       button.removeEventListener("command", this._onViewToggle);
     }
 
-    yield WaterfallView.destroy();
-    yield CallTreeView.destroy();
-    yield FlameGraphView.destroy();
+    for (let [_, { view }] of Iterator(this.components)) {
+      yield view.destroy();
+    }
   }),
 
   /**
    * Select one of the DetailView's subviews to be rendered,
    * hiding the others.
    *
    * @param String viewName
    *        Name of the view to be shown.
    */
   selectView: function (viewName) {
-    this.el.selectedIndex = this.components[viewName].index;
+    this.el.selectedPanel = $("#" + this.components[viewName].id);
 
     for (let button of $$("toolbarbutton[data-view]", this.toolbar)) {
       if (button.getAttribute("data-view") === viewName) {
         button.setAttribute("checked", true);
       } else {
         button.removeAttribute("checked");
       }
     }
@@ -75,20 +75,21 @@ let DetailsView = {
 
   /**
    * Checks if the provided view is currently selected.
    *
    * @param object viewObject
    * @return boolean
    */
   isViewSelected: function(viewObject) {
-    let selectedIndex = this.el.selectedIndex;
+    let selectedPanel = this.el.selectedPanel;
+    let selectedId = selectedPanel.id;
 
-    for (let [, { index, view }] of Iterator(this.components)) {
-      if (index == selectedIndex && view == viewObject) {
+    for (let [, { id, view }] of Iterator(this.components)) {
+      if (id == selectedId && view == viewObject) {
         return true;
       }
     }
 
     return false;
   },
 
   /**
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -19,16 +19,17 @@ support-files =
 [browser_flame-graph-02.js]
 [browser_flame-graph-03a.js]
 [browser_flame-graph-03b.js]
 [browser_flame-graph-04.js]
 [browser_flame-graph-utils-01.js]
 [browser_flame-graph-utils-02.js]
 [browser_flame-graph-utils-03.js]
 [browser_flame-graph-utils-04.js]
+[browser_flame-graph-utils-05.js]
 [browser_flame-graph-utils-hash.js]
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-05.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that flame graph data is cached, and that the cache may be cleared.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+  let out1 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+  let out2 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+  is(out1, out2, "The outputted data is identical.")
+
+  let out3 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, { flattenRecursion: true });
+  is(out2, out3, "The outputted data is still identical.");
+
+  FlameGraphUtils.removeFromCache(TEST_DATA);
+  let out4 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, { flattenRecursion: true });
+  isnot(out3, out4, "The outputted data is not identical anymore.");
+}
+
+let TEST_DATA = [{
+  frames: [{
+    location: "A"
+  }, {
+    location: "A"
+  }, {
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "B",
+  }, {
+    location: "C"
+  }],
+  time: 50,
+}];
--- a/browser/devtools/shared/widgets/FlameGraph.jsm
+++ b/browser/devtools/shared/widgets/FlameGraph.jsm
@@ -851,36 +851,47 @@ const COLOR_PALLETTE = Array.from(Array(
   ")"
 );
 
 /**
  * A collection of utility functions converting various data sources
  * into a format drawable by the FlameGraph.
  */
 let FlameGraphUtils = {
+  _cache: new WeakMap(),
+
   /**
    * Converts a list of samples from the profiler data to something that's
    * drawable by a FlameGraph widget.
    *
+   * The outputted data will be cached, so the next time this method is called
+   * the previous output is returned. If this is undesirable, or should the
+   * options change, use `removeFromCache`.
+   *
    * @param array samples
    *        A list of { time, frames: [{ location }] } objects.
    * @param object options [optional]
    *        Additional options supported by this operation:
    *          - flattenRecursion: specifies if identical consecutive frames
    *                              should be omitted from the output
    *          - filterFrames: predicate used for filtering all frames, passing
    *                          in each frame, its index and the sample array
    *          - showIdleBlocks: adds "idle" blocks when no frames are available
    *                            using the provided localized text
    * @param array out [optional]
    *        An output storage to reuse for storing the flame graph data.
    * @return array
    *         The flame graph data.
    */
   createFlameGraphDataFromSamples: function(samples, options = {}, out = []) {
+    let cached = this._cache.get(samples);
+    if (cached) {
+      return cached;
+    }
+
     // 1. Create a map of colors to arrays, representing buckets of
     // blocks inside the flame graph pyramid sharing the same style.
 
     let buckets = new Map();
 
     for (let color of COLOR_PALLETTE) {
       buckets.set(color, []);
     }
@@ -947,20 +958,29 @@ let FlameGraphUtils = {
 
     // 3. Convert the buckets into a data source usable by the FlameGraph.
     // This is a simple conversion from a Map to an Array.
 
     for (let [color, blocks] of buckets) {
       out.push({ color, blocks });
     }
 
+    this._cache.set(samples, out);
     return out;
   },
 
   /**
+   * Clears the cached flame graph data created for the given source.
+   * @param any source
+   */
+  removeFromCache: function(source) {
+    this._cache.delete(source);
+  },
+
+  /**
    * Checks if the provided frame is the same as the next one in a sample.
    *
    * @param object e
    *        An object containing a { location } property.
    * @param number index
    *        The index of the object in the parent array.
    * @param array array
    *        The parent array.
--- a/browser/locales/en-US/chrome/browser/preferences/content.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/content.dtd
@@ -37,8 +37,12 @@
   -                     translation.options.attribution.afterLogo):
   -  These 2 strings are displayed before and after a 'Microsoft Translator'
   -  logo.
   -  The translations for these strings should match the translations in
   -  browser/translation.dtd
   -->
 <!ENTITY translation.options.attribution.beforeLogo "Translations by">
 <!ENTITY translation.options.attribution.afterLogo "">
+
+<!ENTITY  playDRMContent.label         "Play DRM content">
+<!ENTITY  playDRMContent.accesskey     "P">
+<!ENTITY  playDRMContent.learnMore.label "Learn more">
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -40,17 +40,20 @@ let ReaderParent = {
   receiveMessage: function(message) {
     switch (message.name) {
       case "Reader:AddToList":
         // XXX: To implement.
         break;
 
       case "Reader:ArticleGet":
         this._getArticle(message.data.url, message.target).then((article) => {
-          message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
+          // Make sure the target browser is still alive before trying to send data back.
+          if (message.target.messageManager) {
+            message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
+          }
         });
         break;
 
       case "Reader:FaviconRequest": {
         // XXX: To implement.
         break;
       }
       case "Reader:ListStatusRequest":
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -841,8 +841,13 @@ pref("home.sync.checkIntervalSecs", 3600
 // Enable device storage API
 pref("device.storage.enabled", true);
 
 // Enable meta-viewport support for font inflation code
 pref("dom.meta-viewport.enabled", true);
 
 // Enable the OpenH264 plugin support in the addon manager.
 pref("media.gmp-gmpopenh264.provider.enabled", true);
+
+// The default color scheme in reader mode (light, dark, print, auto)
+// auto = color automatically adjusts according to ambient light level
+// (auto only works on platforms where the 'devicelight' event is enabled)
+pref("reader.color_scheme", "auto");
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -282,9 +282,12 @@ public class AppConstants {
 //#endif
 
     public static final boolean MOZ_LINKER_EXTRACT =
 //#ifdef MOZ_LINKER_EXTRACT
     true;
 //#else
     false;
 //#endif
+
+    public static final boolean MOZ_DRAGGABLE_URLBAR = false;
+
 }
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -254,16 +254,185 @@ public class BrowserApp extends GeckoApp
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
     // race by determining if the web content should be hidden at the animation's end.
     private boolean mHideWebContentOnAnimationEnd;
 
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
+    private DragHelper mDragHelper;
+
+    private class DragHelper implements OuterLayout.DragCallback {
+        private int[] mToolbarLocation = new int[2]; // to avoid creation every time we need to check for toolbar location.
+        // When dragging horizontally, the area of mainlayout between left drag bound and right drag bound can
+        // be dragged. A touch on the right of that area will automatically close the view.
+        private int mStatusBarHeight;
+
+        public DragHelper() {
+            // If a layout round happens from the root, the offset placed by viewdraghelper gets forgotten and
+            // main layout gets replaced to offset 0.
+            ((MainLayout) mMainLayout).setLayoutInterceptor(new LayoutInterceptor() {
+                @Override
+                public void onLayout() {
+                    if (mRootLayout.isMoving()) {
+                        mRootLayout.restoreTargetViewPosition();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onDragProgress(float progress) {
+            mBrowserToolbar.setToolBarButtonsAlpha(1.0f - progress);
+            mTabsPanel.translateInRange(progress);
+        }
+
+        @Override
+        public View getViewToDrag() {
+            return mMainLayout;
+        }
+
+        /**
+         * Since pressing the tabs button slides the main layout, whereas draghelper changes its offset, here we
+         * restore the position of mainlayout as if it was opened by pressing the button. This allows the closing
+         * mechanism to work.
+         */
+        @Override
+        public void startDrag(boolean wasOpen) {
+            if (wasOpen) {
+                mTabsPanel.setHWLayerEnabled(true);
+                mMainLayout.offsetTopAndBottom(getDragRange());
+                mMainLayout.scrollTo(0, 0);
+            } else {
+                prepareTabsToShow();
+                mBrowserToolbar.hideVirtualKeyboard();
+            }
+            mBrowserToolbar.setContextMenuEnabled(false);
+        }
+
+        @Override
+        public void stopDrag(boolean stoppingToOpen) {
+            if (stoppingToOpen) {
+                mTabsPanel.setHWLayerEnabled(false);
+                mMainLayout.offsetTopAndBottom(-getDragRange());
+                mMainLayout.scrollTo(0, -getDragRange());
+            } else {
+                mTabsPanel.hideImmediately();
+                mTabsPanel.setHWLayerEnabled(false);
+            }
+            // Re-enabling context menu only while stopping to close.
+            if (stoppingToOpen) {
+                mBrowserToolbar.setContextMenuEnabled(false);
+            } else {
+                mBrowserToolbar.setContextMenuEnabled(true);
+            }
+        }
+
+        @Override
+        public int getDragRange() {
+            return mTabsPanel.getVerticalPanelHeight();
+        }
+
+        @Override
+        public int getOrderedChildIndex(int index) {
+            // See ViewDragHelper's findTopChildUnder method. ViewDragHelper looks for the topmost view in z order
+            // to understand what needs to be dragged. Here we are tampering Toast's index in case it's hidden,
+            // otherwise draghelper would try to drag it.
+            int mainLayoutIndex = mRootLayout.indexOfChild(mMainLayout);
+            if (index > mainLayoutIndex &&  (mToast == null || !mToast.isVisible())) {
+                return mainLayoutIndex;
+            } else {
+                return index;
+            }
+        }
+
+        @Override
+        public boolean canDrag(MotionEvent event) {
+            if (!AppConstants.MOZ_DRAGGABLE_URLBAR) {
+                return false;
+            }
+
+            // if no current tab is active.
+            if (Tabs.getInstance().getSelectedTab() == null) {
+                return false;
+            }
+
+            // currently disabled for tablets.
+            if (HardwareUtils.isTablet()) {
+                return false;
+            }
+
+            // not enabled in editing mode.
+            if (mBrowserToolbar.isEditing()) {
+                return false;
+            }
+
+            return isInToolbarBounds((int) event.getRawY());
+        }
+
+        @Override
+        public boolean canInterceptEventWhileOpen(MotionEvent event) {
+            if (event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+                return false;
+            }
+
+            // Need to check if are intercepting a touch on main layout since we might hit a visible toast.
+            if (mRootLayout.findTopChildUnder(event) == mMainLayout &&
+                isInToolbarBounds((int) event.getRawY())) {
+                return true;
+            }
+            return false;
+        }
+
+        private boolean isInToolbarBounds(int y) {
+            mBrowserToolbar.getLocationOnScreen(mToolbarLocation);
+            final int upperLimit = mToolbarLocation[1] + mBrowserToolbar.getMeasuredHeight();
+            final int lowerLimit = mToolbarLocation[1];
+            return (y > lowerLimit && y < upperLimit);
+        }
+
+        public void prepareTabsToShow() {
+            if (ensureTabsPanelExists()) {
+                // If we've just inflated the tabs panel, only show it once the current
+                // layout pass is done to avoid displayed temporary UI states during
+                // relayout.
+                final ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
+                if (vto.isAlive()) {
+                    vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+                        @Override
+                        public void onGlobalLayout() {
+                            mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+                            prepareTabsToShow();
+                        }
+                    });
+                }
+            } else {
+                mTabsPanel.prepareToDrag();
+            }
+        }
+
+        public int getLowerLimit() {
+            return getStatusBarHeight();
+        }
+
+        private int getStatusBarHeight() {
+            if (mStatusBarHeight != 0) {
+                return mStatusBarHeight;
+            }
+            final int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+            if (resourceId > 0) {
+                mStatusBarHeight = getResources().getDimensionPixelSize(resourceId);
+                return mStatusBarHeight;
+            }
+            Log.e(LOGTAG, "Unable to find statusbar height");
+            return 0;
+        }
+    }
+
     @Override
     public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
         } else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
             view = TabsPanel.createTabsLayout(context, attrs);
         } else {
@@ -653,16 +822,19 @@ public class BrowserApp extends GeckoApp
 
         mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() {
             @Override
             public void onEnabledChanged(boolean enabled) {
                 setDynamicToolbarEnabled(enabled);
             }
         });
 
+        mDragHelper = new DragHelper();
+        mRootLayout.setDraggableCallback(mDragHelper);
+
         // Set the maximum bits-per-pixel the favicon system cares about.
         IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
     }
 
     private void setupSystemUITinting() {
         if (!Versions.feature19Plus) {
             return;
         }
@@ -1353,16 +1525,17 @@ public class BrowserApp extends GeckoApp
         });
     }
 
     @Override
     public void refreshChrome() {
         invalidateOptionsMenu();
 
         if (mTabsPanel != null) {
+            mRootLayout.reset();
             updateSideBarState();
             mTabsPanel.refresh();
         }
 
         mBrowserToolbar.refresh();
     }
 
     @Override
@@ -1376,36 +1549,41 @@ public class BrowserApp extends GeckoApp
             public void run() {
                 if (mDynamicToolbar.isEnabled()) {
                     mDynamicToolbar.setVisible(visible, VisibilityTransition.IMMEDIATE);
                 }
             }
         });
     }
 
+    private boolean isSideBar() {
+        return (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE);
+    }
+
     private void updateSideBarState() {
         if (NewTabletUI.isEnabled(this)) {
             return;
         }
 
         if (mMainLayoutAnimator != null)
             mMainLayoutAnimator.stop();
 
-        boolean isSideBar = (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE);
+        boolean isSideBar = isSideBar();
         final int sidebarWidth = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width);
 
         ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTabsPanel.getLayoutParams();
         lp.width = (isSideBar ? sidebarWidth : ViewGroup.LayoutParams.MATCH_PARENT);
         mTabsPanel.requestLayout();
 
         final boolean sidebarIsShown = (isSideBar && mTabsPanel.isShown());
         final int mainLayoutScrollX = (sidebarIsShown ? -sidebarWidth : 0);
         mMainLayout.scrollTo(mainLayoutScrollX, 0);
 
         mTabsPanel.setIsSideBar(isSideBar);
+        mRootLayout.updateDragHelperParameters();
     }
 
     @Override
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
         if ("Accounts:Create".equals(event)) {
             // Do exactly the same thing as if you tapped 'Sync' in Settings.
             final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class);
@@ -1727,17 +1905,17 @@ public class BrowserApp extends GeckoApp
             // layout pass is done to avoid displayed temporary UI states during
             // relayout.
             ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
             if (vto.isAlive()) {
                 vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                     @Override
                     public void onGlobalLayout() {
                         mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
-                        mTabsPanel.show(panel);
+                        showTabs(panel);
                     }
                 });
             }
         } else {
             mTabsPanel.show(panel);
         }
     }
 
@@ -1816,20 +1994,23 @@ public class BrowserApp extends GeckoApp
     public void onPropertyAnimationStart() {
     }
 
     @Override
     public void onPropertyAnimationEnd() {
         if (!areTabsShown()) {
             mTabsPanel.setVisibility(View.INVISIBLE);
             mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+            mRootLayout.setClosed();
+            mBrowserToolbar.setContextMenuEnabled(true);
         } else {
             // Cancel editing mode to return to page content when the TabsPanel closes. We cancel
             // it here because there are graphical glitches if it's canceled while it's visible.
             mBrowserToolbar.cancelEdit();
+            mRootLayout.setOpen();
         }
 
         mTabsPanel.finishTabsAnimation();
 
         mMainLayoutAnimator = null;
     }
 
     @Override
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -156,18 +156,19 @@ public abstract class GeckoApp
 
     public static final String SAVED_STATE_IN_BACKGROUND   = "inBackground";
     public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
 
     // Delay before running one-time "cleanup" tasks that may be needed
     // after a version upgrade.
     private static final int CLEANUP_DEFERRAL_SECONDS = 15;
 
-    protected RelativeLayout mRootLayout;
+    protected OuterLayout mRootLayout;
     protected RelativeLayout mMainLayout;
+
     protected RelativeLayout mGeckoLayout;
     private View mCameraView;
     private OrientationEventListener mCameraOrientationEventListener;
     public List<GeckoAppShell.AppStateListener> mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
     protected MenuPanel mMenuPanel;
     protected Menu mMenu;
     protected GeckoProfile mProfile;
     protected boolean mIsRestoringActivity;
@@ -1262,17 +1263,17 @@ public abstract class GeckoApp
 
         super.onCreate(savedInstanceState);
 
         GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation);
 
         setContentView(getLayout());
 
         // Set up Gecko layout.
-        mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
+        mRootLayout = (OuterLayout) findViewById(R.id.root_layout);
         mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
         mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
 
         // Determine whether we should restore tabs.
         mShouldRestore = getSessionRestoreState(savedInstanceState);
         if (mShouldRestore && savedInstanceState != null) {
             boolean wasInBackground =
                 savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
@@ -2388,21 +2389,34 @@ public abstract class GeckoApp
     }
 
     public void setAccessibilityEnabled(boolean enabled) {
     }
 
     public static class MainLayout extends RelativeLayout {
         private TouchEventInterceptor mTouchEventInterceptor;
         private MotionEventInterceptor mMotionEventInterceptor;
+        private LayoutInterceptor mLayoutInterceptor;
 
         public MainLayout(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
 
+        @Override
+        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            super.onLayout(changed, left, top, right, bottom);
+            if (mLayoutInterceptor != null) {
+                mLayoutInterceptor.onLayout();
+            }
+        }
+
+        public void setLayoutInterceptor(LayoutInterceptor interceptor) {
+            mLayoutInterceptor = interceptor;
+        }
+
         public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
             mTouchEventInterceptor = interceptor;
         }
 
         public void setMotionEventInterceptor(MotionEventInterceptor interceptor) {
             mMotionEventInterceptor = interceptor;
         }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/LayoutInterceptor.java
@@ -0,0 +1,11 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+
+package org.mozilla.gecko;
+
+public interface LayoutInterceptor {
+    public void onLayout();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/OuterLayout.java
@@ -0,0 +1,254 @@
+/* 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/. */
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+/* Outerlayout is the container layout of all the main views. It allows mainlayout to be dragged while targeting
+   the toolbar and it's responsible for handling the dragprocess. It relies on ViewDragHelper to ease the drag process.
+ */
+public class OuterLayout extends RelativeLayout {
+    private final double AUTO_OPEN_SPEED_LIMIT = 800.0;
+    private ViewDragHelper mDragHelper;
+    private int mDraggingBorder;
+    private int mDragRange;
+    private boolean mIsOpen = false;
+    private int mDraggingState = ViewDragHelper.STATE_IDLE;
+    private DragCallback mDragCallback;
+
+    public static interface DragCallback {
+        public void startDrag(boolean wasOpen);
+        public void stopDrag(boolean stoppingToOpen);
+        public int getDragRange();
+        public int getOrderedChildIndex(int index);
+        public boolean canDrag(MotionEvent event);
+        public boolean canInterceptEventWhileOpen(MotionEvent event);
+        public void onDragProgress(float progress);
+        public View getViewToDrag();
+        public int getLowerLimit();
+    }
+
+    private class DragHelperCallback extends ViewDragHelper.Callback {
+        @Override
+        public void onViewDragStateChanged(int newState) {
+            if (newState == mDraggingState) { // no change
+                return;
+            }
+
+            // if the view stopped moving.
+            if ((mDraggingState == ViewDragHelper.STATE_DRAGGING || mDraggingState == ViewDragHelper.STATE_SETTLING) &&
+                 newState == ViewDragHelper.STATE_IDLE) {
+
+                final float rangeToCheck = mDragRange;
+                final float lowerLimit = mDragCallback.getLowerLimit();
+                if (mDraggingBorder == lowerLimit) {
+                    mIsOpen = false;
+                    mDragCallback.onDragProgress(0);
+                } else if (mDraggingBorder == rangeToCheck) {
+                    mIsOpen = true;
+                    mDragCallback.onDragProgress(1);
+                }
+                mDragCallback.stopDrag(mIsOpen);
+            }
+
+            // The view was previuosly moving.
+            if (newState == ViewDragHelper.STATE_DRAGGING && !isMoving()) {
+                mDragCallback.startDrag(mIsOpen);
+                updateRanges();
+            }
+
+            mDraggingState = newState;
+        }
+
+        @Override
+        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+            mDraggingBorder = top;
+            final float progress = Math.min(1, ((float) top) / mDragRange);
+            mDragCallback.onDragProgress(progress);
+        }
+
+        @Override
+        public int getViewVerticalDragRange(View child) {
+            return mDragRange;
+        }
+
+        @Override
+        public int getOrderedChildIndex(int index) {
+            return mDragCallback.getOrderedChildIndex(index);
+        }
+
+        @Override
+        public boolean tryCaptureView(View view, int i) {
+            return (view.getId() == mDragCallback.getViewToDrag().getId());
+        }
+
+        @Override
+        public int clampViewPositionVertical(View child, int top, int dy) {
+            return top;
+        }
+
+        @Override
+        public void onViewReleased(View releasedChild, float xvel, float yvel) {
+            final float rangeToCheck = mDragRange;
+            final float speedToCheck = yvel;
+
+            if (mDraggingBorder == mDragCallback.getLowerLimit()) {
+                return;
+            }
+
+            if (mDraggingBorder == rangeToCheck) {
+                return;
+            }
+
+            boolean settleToOpen = false;
+            // Speed has priority over position.
+            if (speedToCheck > AUTO_OPEN_SPEED_LIMIT) {
+                settleToOpen = true;
+            } else if (speedToCheck < -AUTO_OPEN_SPEED_LIMIT) {
+                settleToOpen = false;
+            } else if (mDraggingBorder > rangeToCheck / 2) {
+                settleToOpen = true;
+            } else if (mDraggingBorder < rangeToCheck / 2) {
+                settleToOpen = false;
+            }
+
+            final int settleDestX;
+            final int settleDestY;
+            if (settleToOpen) {
+                settleDestX = 0;
+                settleDestY = mDragRange;
+            } else {
+                settleDestX = 0;
+                settleDestY = mDragCallback.getLowerLimit();
+            }
+
+            if(mDragHelper.settleCapturedViewAt(settleDestX, settleDestY)) {
+                ViewCompat.postInvalidateOnAnimation(OuterLayout.this);
+            }
+        }
+    }
+
+    public OuterLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    private void updateRanges() {
+        // Need to wait for the tabs to show in order to fetch the right sizes.
+        mDragRange = mDragCallback.getDragRange() + mDragCallback.getLowerLimit();
+    }
+
+    private void updateOrientation() {
+        mDragHelper.setEdgeTrackingEnabled(0);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
+        mIsOpen = false;
+        super.onFinishInflate();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mDragCallback.canDrag(event)) {
+            if (mDragHelper.shouldInterceptTouchEvent(event)) {
+                return true;
+            }
+        }
+
+        // Because while open the target layout is translated and draghelper does not catch it.
+        if (mIsOpen && mDragCallback.canInterceptEventWhileOpen(event)) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // touch events can be passed to the helper if we target the toolbar or we are already dragging.
+        if (mDragCallback.canDrag(event) || mDraggingState == ViewDragHelper.STATE_DRAGGING) {
+            mDragHelper.processTouchEvent(event);
+        }
+        return true;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        // The first time fennec is started, tabs might not have been created while we drag. In that case we need
+        // an arbitrary range to start dragging that will be updated as soon as the tabs are created.
+
+        if (mDragRange == 0) {
+            mDragRange = h / 2;
+        }
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    @Override
+    public void computeScroll() { // needed for automatic settling.
+        if (mDragHelper.continueSettling(true)) {
+            ViewCompat.postInvalidateOnAnimation(this);
+        }
+    }
+
+    /**
+     * To be called when closing the tabs from outside (i.e. when touching the main layout).
+     */
+    public void setClosed() {
+        mIsOpen = false;
+        mDragHelper.abort();
+    }
+
+    /**
+     * To be called when opening the tabs from outside (i.e. when clicking on the tabs button).
+     */
+    public void setOpen() {
+        mIsOpen = true;
+        mDragHelper.abort();
+    }
+
+    public void setDraggableCallback(DragCallback dragCallback) {
+        mDragCallback = dragCallback;
+        updateOrientation();
+    }
+
+    // If a change happens while we are dragging, we abort the dragging and set to open state.
+    public void reset() {
+        updateOrientation();
+        if (isMoving()) {
+            mDragHelper.abort();
+            if (mDragCallback != null) {
+                mDragCallback.stopDrag(false);
+                mDragCallback.onDragProgress(0f);
+            }
+        }
+    }
+
+    public void updateDragHelperParameters() {
+        mDragRange = mDragCallback.getDragRange() + mDragCallback.getLowerLimit();
+        updateOrientation();
+    }
+
+    public boolean isMoving() {
+        return (mDraggingState == ViewDragHelper.STATE_DRAGGING ||
+                mDraggingState == ViewDragHelper.STATE_SETTLING);
+    }
+
+    public boolean isOpen() {
+        return mIsOpen;
+    }
+
+    public View findTopChildUnder(MotionEvent event) {
+        return mDragHelper.findTopChildUnder((int) event.getX(), (int) event.getY());
+    }
+
+    public void restoreTargetViewPosition() {
+        mDragCallback.getViewToDrag().offsetTopAndBottom(mDraggingBorder);
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -328,16 +328,17 @@ gbjar.sources += [
     'home/TopSitesGridView.java',
     'home/TopSitesPanel.java',
     'home/TopSitesThumbnailView.java',
     'home/TransitionAwareCursorLoaderCallbacks.java',
     'home/TwoLinePageRow.java',
     'InputMethods.java',
     'IntentHelper.java',
     'JavaAddonManager.java',
+    'LayoutInterceptor.java',
     'LocaleManager.java',
     'Locales.java',
     'lwt/LightweightTheme.java',
     'lwt/LightweightThemeDrawable.java',
     'MediaCastingBar.java',
     'MemoryMonitor.java',
     'menu/GeckoMenu.java',
     'menu/GeckoMenuInflater.java',
@@ -351,16 +352,17 @@ gbjar.sources += [
     'MotionEventInterceptor.java',
     'NewTabletUI.java',
     'NotificationClient.java',
     'NotificationHandler.java',
     'NotificationHelper.java',
     'NotificationService.java',
     'NSSBridge.java',
     'OrderedBroadcastHelper.java',
+    'OuterLayout.java',
     'preferences/AlignRightLinkPreference.java',
     'preferences/AndroidImport.java',
     'preferences/AndroidImportPreference.java',
     'preferences/ClearOnShutdownPref.java',
     'preferences/CustomCheckBoxPreference.java',
     'preferences/CustomListCategory.java',
     'preferences/CustomListPreference.java',
     'preferences/FontSizePreference.java',
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -1,14 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<org.mozilla.gecko.OuterLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:gecko="http://schemas.android.com/apk/res-auto"
                 android:id="@+id/root_layout"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:fitsSystemWindows="true">
 
     <ViewStub android:id="@+id/tabs_panel"
               android:layout="@layout/tabs_panel_view"
@@ -129,9 +129,9 @@
                  android:layout_height="match_parent"
                  android:layout_alignParentBottom="true"
                  android:visibility="gone" />
 
     <ViewStub android:id="@+id/toast_stub"
               android:layout="@layout/button_toast"
               style="@style/Toast"/>
 
-</RelativeLayout>
+</org.mozilla.gecko.OuterLayout>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -187,16 +187,18 @@
     <dimen name="tab_history_combo_margin_left">15dp</dimen>
     <dimen name="tab_history_combo_margin_right">15dp</dimen>
     <dimen name="tab_history_title_fading_width">50dp</dimen>
     <dimen name="tab_history_title_margin_right">15dp</dimen>
     <dimen name="tab_history_title_text_size">14sp</dimen>
     <dimen name="tab_history_bg_width">2dp</dimen>
     <dimen name="tab_history_border_padding">2dp</dimen>
 
+    <dimen name="horizontal_drag_area">256dp</dimen>
+
     <!-- Find-In-Page dialog dimensions. -->
     <dimen name="find_in_page_text_margin_left">5dip</dimen>
     <dimen name="find_in_page_text_margin_right">12dip</dimen>
     <dimen name="find_in_page_text_padding_left">10dip</dimen>
     <dimen name="find_in_page_text_padding_right">10dip</dimen>
     <dimen name="find_in_page_status_margin_right">10dip</dimen>
     <dimen name="find_in_page_matchcase_padding">10dip</dimen>
     <dimen name="find_in_page_control_margin_top">2dip</dimen>
--- a/mobile/android/base/tabs/TabsPanel.java
+++ b/mobile/android/base/tabs/TabsPanel.java
@@ -9,16 +9,18 @@ import org.mozilla.gecko.AppConstants.Ve
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.NewTabletUI;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.lwt.LightweightThemeDrawable;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
 import org.mozilla.gecko.widget.IconTabWidget;
 
 import android.content.Context;
@@ -388,25 +390,50 @@ public class TabsPanel extends LinearLay
         @Override
         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
             super.onLayout(changed, left, top, right, bottom);
             onLightweightThemeChanged();
         }
     }
 
     public void show(Panel panelToShow) {
-        if (!isShown())
+        final boolean showAnimation = !mVisible;
+        prepareToShow(panelToShow);
+        if (isSideBar()) {
+            if (showAnimation) {
+                dispatchLayoutChange(getWidth(), getHeight());
+            }
+        } else {
+            int height = getVerticalPanelHeight();
+            dispatchLayoutChange(getWidth(), height);
+        }
+    }
+
+    public void prepareToDrag() {
+        Tab selectedTab = Tabs.getInstance().getSelectedTab();
+        if (selectedTab != null && selectedTab.isPrivate()) {
+            prepareToShow(TabsPanel.Panel.PRIVATE_TABS);
+        } else {
+            prepareToShow(TabsPanel.Panel.NORMAL_TABS);
+        }
+        if (mIsSideBar) {
+            prepareSidebarAnimation(getWidth());
+        }
+    }
+
+    public void prepareToShow(Panel panelToShow) {
+        if (!isShown()) {
             setVisibility(View.VISIBLE);
+        }
 
         if (mPanel != null) {
             // Hide the old panel.
             mPanel.hide();
         }
 
-        final boolean showAnimation = !mVisible;
         mVisible = true;
         mCurrentPanel = panelToShow;
 
         int index = panelToShow.ordinal();
         mTabWidget.setCurrentTab(index);
 
         switch (panelToShow) {
             case NORMAL_TABS:
@@ -426,30 +453,30 @@ public class TabsPanel extends LinearLay
         }
 
         mAddTab.setVisibility(View.VISIBLE);
         mAddTab.setImageLevel(index);
 
         if (!HardwareUtils.hasMenuButton()) {
             mMenuButton.setVisibility(View.VISIBLE);
             mMenuButton.setEnabled(true);
-            mPopupMenu.setAnchor(mMenuButton);
         } else {
             mPopupMenu.setAnchor(mAddTab);
         }
+    }
 
-        if (isSideBar()) {
-            if (showAnimation)
-                dispatchLayoutChange(getWidth(), getHeight());
-        } else {
-            int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
-            int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
-            dispatchLayoutChange(getWidth(), height);
-        }
-        mHeaderVisible = true;
+    public void hideImmediately() {
+        mVisible = false;
+        setVisibility(View.INVISIBLE);
+    }
+
+    public int getVerticalPanelHeight() {
+        final int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
+        final int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
+        return height;
     }
 
     public void hide() {
         mHeaderVisible = false;
 
         if (mVisible) {
             mVisible = false;
             mPopupMenu.dismiss();
@@ -483,32 +510,48 @@ public class TabsPanel extends LinearLay
     public void setIsSideBar(boolean isSideBar) {
         mIsSideBar = isSideBar;
     }
 
     public Panel getCurrentPanel() {
         return mCurrentPanel;
     }
 
+    public void setHWLayerEnabled(boolean enabled) {
+        if (Versions.preHC) {
+            return;
+        }
+        if (enabled) {
+            mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        } else {
+            mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
+            mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+        }
+    }
+
+    public void prepareSidebarAnimation(int tabsPanelWidth) {
+        if (mVisible) {
+            ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
+            ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
+            // The footer view is only present on the sidebar, v11+.
+            ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
+        }
+    }
+
     public void prepareTabsAnimation(PropertyAnimator animator) {
         // Not worth doing this on pre-Honeycomb without proper
         // hardware accelerated animations.
         if (Versions.preHC) {
             return;
         }
 
         if (mIsSideBar) {
             final int tabsPanelWidth = getWidth();
-            if (mVisible) {
-                ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
-                ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
-
-                // The footer view is only present on the sidebar, v11+.
-                ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
-            }
+            prepareSidebarAnimation(tabsPanelWidth);
             final int translationX = (mVisible ? 0 : -tabsPanelWidth);
             animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
             animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX);
             animator.attach(mFooter, PropertyAnimator.Property.TRANSLATION_X, translationX);
 
         } else if (!mHeaderVisible) {
             final Resources resources = getContext().getResources();
             final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
@@ -518,29 +561,45 @@ public class TabsPanel extends LinearLay
                 ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight);
                 ViewHelper.setAlpha(mTabsContainer, 0.0f);
             }
             animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
             animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
             animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
         }
 
-        mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-        mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        setHWLayerEnabled(true);
+    }
+
+    public void translateInRange(float progress) {
+        final Resources resources = getContext().getResources();
+        if (!mIsSideBar) {
+            final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
+            final int translationY =  (int) - ((1 - progress) * toolbarHeight);
+            ViewHelper.setTranslationY(mHeader, translationY);
+            ViewHelper.setTranslationY(mTabsContainer, translationY);
+            mTabsContainer.setAlpha(progress);
+        } else {
+            final int tabsPanelWidth = getWidth();
+            prepareSidebarAnimation(tabsPanelWidth);
+            final int translationX = (int) - ((1 - progress) * tabsPanelWidth);
+            ViewHelper.setTranslationX(mHeader, translationX);
+            ViewHelper.setTranslationX(mTabsContainer, translationX);
+            ViewHelper.setTranslationX(mFooter, translationX);
+        }
     }
 
     public void finishTabsAnimation() {
         if (Versions.preHC) {
             return;
         }
 
-        mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
-        mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+        setHWLayerEnabled(false);
 
-        // If the tabs panel is now hidden, call hide() on current panel and unset it as the current panel
+        // If the tray is now hidden, call hide() on current panel and unset it as the current panel
         // to avoid hide() being called again when the layout is opened next.
         if (!mVisible && mPanel != null) {
             mPanel.hide();
             mPanel = null;
         }
     }
 
     public void setTabsLayoutChangeListener(TabsLayoutChangeListener listener) {
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -135,16 +135,17 @@ public abstract class BrowserToolbar ext
 
     protected UIMode uiMode;
     protected TabHistoryController tabHistoryController;
 
     private final Paint shadowPaint;
     private final int shadowSize;
 
     private final ToolbarPrefs prefs;
+    private boolean contextMenuEnabled = true;
 
     public abstract boolean isAnimating();
 
     protected abstract boolean isTabsButtonOffscreen();
 
     protected abstract void updateNavigationButtons(Tab tab);
 
     protected abstract void triggerStartEditingTransition(PropertyAnimator animator);
@@ -238,18 +239,18 @@ public abstract class BrowserToolbar ext
                     activateListener.onActivate();
                 }
             }
         });
 
         setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
             @Override
             public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
-                // We don't the context menu while editing
-                if (isEditing()) {
+                // We don't the context menu while editing or while dragging
+                if (isEditing() || !contextMenuEnabled) {
                     return;
                 }
 
                 // NOTE: Use MenuUtils.safeSetVisible because some actions might
                 // be on the Page menu
 
                 MenuInflater inflater = activity.getMenuInflater();
                 inflater.inflate(R.menu.titlebar_contextmenu, menu);
@@ -564,26 +565,29 @@ public abstract class BrowserToolbar ext
     @Override
     public void setNextFocusDownId(int nextId) {
         super.setNextFocusDownId(nextId);
         tabsButton.setNextFocusDownId(nextId);
         urlDisplayLayout.setNextFocusDownId(nextId);
         menuButton.setNextFocusDownId(nextId);
     }
 
+    public void hideVirtualKeyboard() {
+        InputMethodManager imm =
+                (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+        imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
+    }
+
     private void toggleTabs() {
         if (activity.areTabsShown()) {
             if (activity.hasTabsSideBar())
                 activity.hideTabs();
         } else {
-            // hide the virtual keyboard
-            InputMethodManager imm =
-                    (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
-            imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
 
+            hideVirtualKeyboard();
             Tab tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
                 if (!tab.isPrivate())
                     activity.showNormalTabs();
                 else
                     activity.showPrivateTabs();
             }
         }
@@ -668,16 +672,23 @@ public abstract class BrowserToolbar ext
             }
         }
 
         if (needsNewFocus) {
             requestFocus();
         }
     }
 
+    public void setToolBarButtonsAlpha(float alpha) {
+        ViewHelper.setAlpha(tabsCounter, alpha);
+        if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
+            ViewHelper.setAlpha(menuIcon, alpha);
+        }
+    }
+
     public void onEditSuggestion(String suggestion) {
         if (!isEditing()) {
             return;
         }
 
         urlEditLayout.onEditSuggestion(suggestion);
     }
 
@@ -943,16 +954,20 @@ public abstract class BrowserToolbar ext
         final LightweightThemeDrawable drawable = theme.getColorDrawable(view, color);
         if (drawable != null) {
             drawable.setAlpha(LIGHTWEIGHT_THEME_INVERT_ALPHA, LIGHTWEIGHT_THEME_INVERT_ALPHA);
         }
 
         return drawable;
     }
 
+    public void setContextMenuEnabled(boolean enabled) {
+        contextMenuEnabled = enabled;
+    }
+
     public static class TabEditingState {
         // The edited text from the most recent time this tab was unselected.
         protected String lastEditingText;
         protected int selectionStart;
         protected int selectionEnd;
 
         public boolean isBrowserSearchShown;
 
--- a/mobile/android/base/toolbar/BrowserToolbarNewTablet.java
+++ b/mobile/android/base/toolbar/BrowserToolbarNewTablet.java
@@ -168,16 +168,22 @@ class BrowserToolbarNewTablet extends Br
     }
 
     @Override
     public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
         // Do nothing.
     }
 
     @Override
+    public void setToolBarButtonsAlpha(float alpha) {
+        // Do nothing;
+    }
+
+
+    @Override
     public void startEditing(final String url, final PropertyAnimator animator) {
         // We already know the forward button state - no need to store it here.
         backButtonWasEnabledOnStartEditing = backButton.isEnabled();
 
         setButtonEnabled(backButton, false);
         setButtonEnabled(forwardButton, false);
 
         super.startEditing(url, animator);
--- a/mobile/android/base/toolbar/NavButton.java
+++ b/mobile/android/base/toolbar/NavButton.java
@@ -53,39 +53,34 @@ abstract class NavButton extends ShapedB
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
 
         // Draw the border on top.
         canvas.drawPath(mBorderPath, mBorderPaint);
     }
 
-    // The drawable is constructed as per @drawable/url_bar_nav_button.
+    // The drawable is constructed as per @drawable/new_tablet_url_bar_nav_button.
     @Override
     public void onLightweightThemeChanged() {
-        final Drawable drawable;
-        if (!NewTabletUI.isEnabled(getContext())) {
-            drawable = getTheme().getDrawable(this);
-        } else {
-            drawable = BrowserToolbar.getLightweightThemeDrawable(this, getResources(), getTheme(),
-                    R.color.background_normal);
-        }
+        final Drawable drawable = BrowserToolbar.getLightweightThemeDrawable(this, getResources(),
+                getTheme(), R.color.background_normal);
 
         if (drawable == null) {
             return;
         }
 
         final StateListDrawable stateList = new StateListDrawable();
-        stateList.addState(PRIVATE_PRESSED_STATE_SET, getColorDrawable(R.color.highlight_nav_pb));
-        stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.highlight_nav));
-        stateList.addState(PRIVATE_FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_nav_focused_pb));
-        stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.highlight_nav_focused));
+        stateList.addState(PRIVATE_PRESSED_STATE_SET, getColorDrawable(R.color.new_tablet_highlight_pb));
+        stateList.addState(PRESSED_ENABLED_STATE_SET, getColorDrawable(R.color.new_tablet_highlight));
+        stateList.addState(PRIVATE_FOCUSED_STATE_SET, getColorDrawable(R.color.new_tablet_highlight_focused_pb));
+        stateList.addState(FOCUSED_STATE_SET, getColorDrawable(R.color.new_tablet_highlight_focused));
         stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private));
         stateList.addState(EMPTY_STATE_SET, drawable);
 
         setBackgroundDrawable(stateList);
     }
 
     @Override
     public void onLightweightThemeReset() {
-        setBackgroundResource(R.drawable.url_bar_nav_button);
+        setBackgroundResource(R.drawable.new_tablet_url_bar_nav_button);
     }
 }
--- a/mobile/android/base/widget/ButtonToast.java
+++ b/mobile/android/base/widget/ButtonToast.java
@@ -168,9 +168,13 @@ public class ButtonToast {
     }
 
     private final Runnable mHideRunnable = new Runnable() {
         @Override
         public void run() {
             hide(false, ReasonHidden.TIMEOUT);
         }
     };
+
+    public boolean isVisible() {
+        return (mView.getVisibility() == View.VISIBLE);
+    }
 }
--- a/mobile/android/chrome/content/content.js
+++ b/mobile/android/chrome/content/content.js
@@ -61,22 +61,25 @@ let AboutReaderListener = {
           return;
         }
 
         // Reader mode is disabled until proven enabled.
         this._savedArticle = null;
         sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
 
         ReaderMode.parseDocument(content.document).then(article => {
+          // Do nothing if there is no article, or if the content window has been destroyed.
+          if (article === null || content === null) {
+            return;
+          }
+
           // The loaded page may have changed while we were parsing the document.
           // Make sure we've got the current one.
           let currentURL = Services.io.newURI(content.document.documentURI, null, null).specIgnoringRef;
-
-          // Do nothing if there's no article or the page in this tab has changed.
-          if (article == null || (article.url != currentURL)) {
+          if (article.url !== currentURL) {
             return;
           }
 
           this._savedArticle = article;
           sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
 
         }).catch(e => Cu.reportError("Error parsing document: " + e));
         break;
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java
@@ -335,16 +335,21 @@ public class SearchActivity extends Loca
 
         set.addListener(new Animator.AnimatorListener() {
             @Override
             public void onAnimationStart(Animator animation) {
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
+                // Don't do anything if the activity is destroyed before the animation ends.
+                if (SearchActivity.this.isDestroyed()) {
+                    return;
+                }
+
                 setEditState(EditState.WAITING);
                 setSearchState(SearchState.POSTSEARCH);
 
                 // We need to manually clear the animation for the views to be hidden on gingerbread.
                 animationCard.clearAnimation();
                 animationCard.setVisibility(View.INVISIBLE);
             }
 
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4531,19 +4531,20 @@ pref("reader.parse-on-load.enabled", tru
 
 // Force-enables reader mode parsing, even on low-memory platforms, where it
 // is disabled by default.
 pref("reader.parse-on-load.force-enabled", false);
 
 // The default relative font size in reader mode (1-5)
 pref("reader.font_size", 3);
 
-// The default color scheme in reader mode (light, dark, auto)
+// The default color scheme in reader mode (light, dark, print, auto)
 // auto = color automatically adjusts according to ambient light level
-pref("reader.color_scheme", "auto");
+// (auto only works on platforms where the 'devicelight' event is enabled)
+pref("reader.color_scheme", "light");
 
 // The font type in reader (sans-serif, serif)
 pref("reader.font_type", "sans-serif");
 
 // Whether or not the user has interacted with the reader mode toolbar.
 // This is used to show a first-launch tip in reader mode.
 pref("reader.has_used_toolbar", false);
 
--- a/security/manager/ssl/src/nsNSSCertificate.cpp
+++ b/security/manager/ssl/src/nsNSSCertificate.cpp
@@ -967,34 +967,32 @@ nsNSSCertificate::GetSubjectName(nsAStri
 {
   nsNSSShutDownPreventionLock locker;
   if (isAlreadyShutDown())
     return NS_ERROR_NOT_AVAILABLE;
 
   _subjectName.Truncate();
   if (mCert->subjectName) {
     _subjectName = NS_ConvertUTF8toUTF16(mCert->subjectName);
-    return NS_OK;
   }
-  return NS_ERROR_FAILURE;
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNSSCertificate::GetIssuerName(nsAString& _issuerName)
 {
   nsNSSShutDownPreventionLock locker;
   if (isAlreadyShutDown())
     return NS_ERROR_NOT_AVAILABLE;
 
   _issuerName.Truncate();
   if (mCert->issuerName) {
     _issuerName = NS_ConvertUTF8toUTF16(mCert->issuerName);
-    return NS_OK;
   }
-  return NS_ERROR_FAILURE;
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNSSCertificate::GetSerialNumber(nsAString& _serialNumber)
 {
   nsNSSShutDownPreventionLock locker;
   if (isAlreadyShutDown())
     return NS_ERROR_NOT_AVAILABLE;
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp
@@ -38,17 +38,19 @@ nsParentalControlsService::GetBlockFileD
   *aResult = !res;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsParentalControlsService::GetLoggingEnabled(bool *aResult)
 {
-  return NS_ERROR_NOT_AVAILABLE;
+  // Android doesn't currently have any method of logging restricted actions.
+  *aResult = false;
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsParentalControlsService::Log(int16_t aEntryType,
                                bool aBlocked,
                                nsIURI *aSource,
                                nsIFile *aTarget)
 {
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -585,20 +585,20 @@ AboutReader.prototype = {
 
     this._headerElement.style.display = "block";
 
     let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
     let contentFragment = parserUtils.parseFragment(article.content, Ci.nsIParserUtils.SanitizerDropForms,
                                                     false, articleUri, this._contentElement);
     this._contentElement.innerHTML = "";
     this._contentElement.appendChild(contentFragment);
-    this._updateImageMargins();
     this._maybeSetTextDirection(article);
 
     this._contentElement.style.display = "block";
+    this._updateImageMargins();
     this._requestReadingListStatus();
 
     this._toolbarEnabled = true;
     this._setToolbarVisibility(true);
 
     this._requestFavicon();
   },
 
--- a/toolkit/content/aboutSupport.js
+++ b/toolkit/content/aboutSupport.js
@@ -36,17 +36,19 @@ let snapshotFormatters = {
   application: function application(data) {
     $("application-box").textContent = data.name;
     $("useragent-box").textContent = data.userAgent;
     $("supportLink").href = data.supportURL;
     let version = data.version;
     if (data.vendor)
       version += " (" + data.vendor + ")";
     $("version-box").textContent = version;
-    $("multiprocess-box").textContent = data.numRemoteWindows + "/" + data.numTotalWindows;
+
+    $("multiprocess-box").textContent = stringBundle().formatStringFromName("multiProcessStatus",
+      [data.numRemoteWindows, data.numTotalWindows, data.remoteAutoStart], 3);
   },
 
 #ifdef MOZ_CRASHREPORTER
   crashes: function crashes(data) {
     let strings = stringBundle();
     let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000);
     $("crashes-title").textContent =
       PluralForm.get(daysRange, strings.GetStringFromName("crashesTitle"))
--- a/toolkit/content/widgets/menulist.xml
+++ b/toolkit/content/widgets/menulist.xml
@@ -603,13 +603,13 @@
       </xul:hbox>
       <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
       <children includes="menupopup"/>
     </content>
   </binding>
 
   <binding id="menulist-popuponly" display="xul:menu"
            extends="chrome://global/content/bindings/menulist.xml#menulist">
-    <content>
+    <content sizetopopup="pref">
       <children includes="menupopup"/>
     </content>
   </binding>
 </bindings>
--- a/toolkit/locales/en-US/chrome/global/aboutSupport.properties
+++ b/toolkit/locales/en-US/chrome/global/aboutSupport.properties
@@ -81,8 +81,13 @@ isGPU2Active = GPU #2 Active
 webglRenderer = WebGL Renderer
 
 minLibVersions = Expected minimum version
 loadedLibVersions = Version in use
 
 hasSeccompBPF = Seccomp-BPF (System Call Filtering)
 canSandboxContent = Content Process Sandboxing
 canSandboxMedia = Media Plugin Sandboxing
+
+# LOCALIZATION NOTE %1$S and %2$S will be replaced with the number of remote and the total number
+# of windows, respectively, while %3$S will indicate whether windows are remote by default ('true'
+# or 'false')
+multiProcessStatus = %1$S/%2$S (default: %3$S)
--- a/toolkit/modules/Troubleshoot.jsm
+++ b/toolkit/modules/Troubleshoot.jsm
@@ -156,16 +156,18 @@ let dataProviders = {
                    getInterface(Ci.nsIWebNavigation).
                    QueryInterface(Ci.nsILoadContext).
                    useRemoteTabs;
       if (remote) {
         data.numRemoteWindows++;
       }
     }
 
+    data.remoteAutoStart = Services.appinfo.browserTabsRemoteAutostart;
+
     done(data);
   },
 
 #ifdef MOZ_CRASHREPORTER
   crashes: function crashes(done) {
     let reports = CrashReports.getReports();
     let now = new Date();
     let reportsNew = reports.filter(report => (now - report.date < Troubleshoot.kMaxCrashAge));
--- a/toolkit/modules/tests/browser/browser_Troubleshoot.js
+++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js
@@ -93,16 +93,20 @@ const SNAPSHOT_SCHEMA = {
           type: "string",
         },
         vendor: {
           type: "string",
         },
         supportURL: {
           type: "string",
         },
+        remoteAutoStart: {
+          type: "boolean",
+          required: true,
+        },
         numTotalWindows: {
           type: "number",
         },
         numRemoteWindows: {
           type: "number",
         },
       },
     },
--- a/toolkit/themes/windows/global/aboutReader.css
+++ b/toolkit/themes/windows/global/aboutReader.css
@@ -1,3 +1,195 @@
 /* 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/. */
+
+body {
+  padding: 64px 0;
+  max-width: 660px;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.light {
+  color: #333333;
+  background-color: #ffffff;
+}
+
+.dark {
+  color: #eeeeee;
+  background-color: #333333;
+}
+
+.print {
+  color: #333333;
+  background-color: #fff1df;
+}
+
+.sans-serif {
+  font-family: sans-serif;
+}
+
+.serif {
+  font-family: serif;
+}
+
+/* Loading/error message */
+
+.message {
+  margin-top: 40px;
+  display: none;
+  text-align: center;
+  width: 100%;
+  font-size: 16px;
+}
+
+/* Header */
+
+.header {
+  text-align: start;
+  display: none;
+}
+
+.domain {
+  font-size: 16px;
+  line-height: 24px;
+  padding-bottom: 4px;
+  font-family: sans-serif;
+  text-decoration: none;
+  border-bottom: 1px solid;
+  color: #0095dd;
+}
+
+.light > .header > .domain,
+.print > .header > .domain {
+  border-bottom-color: #333333;
+}
+
+.dark > .header > .domain {
+  border-bottom-color: #eeeeee;
+}
+
+.header > h1 {
+  font-size: 24px;
+  line-height: 30px;
+  width: 100%;
+  margin: 30px 0;
+  padding: 0;
+}
+
+.header > .credits {
+  font-size: 16px;
+  line-height: 24px;
+  margin: 0 0 30px 0;
+  padding: 0;
+  font-style: italic;
+}
+
+/* Content */
+
+.content {
+  display: none;
+  font-size: 18px;
+  line-height: 26px;
+}
+
+.content h1,
+.content h2,
+.content h3 {
+  font-weight: bold;
+}
+
+.content h1 {
+  font-size: 24px;
+  line-height: 30px;
+}
+
+.content h2 {
+  font-size: 20px;
+  line-height: 26px;
+}
+
+.content h3 {
+  font-size: 18px;
+  line-height: 26px;
+}
+
+.content a {
+  text-decoration: underline;
+  font-weight: normal;
+}
+
+.content a,
+.content a:visited,
+.content a:hover,
+.content a:active {
+  color: #0095dd;
+}
+
+.content * {
+  max-width: 100%;
+  height: auto;
+}
+
+.content p,
+.content img,
+.content code,
+.content pre,
+.content blockquote,
+.content ul,
+.content ol,
+.content li {
+  margin: 0 0 30px 0;
+}
+
+.content .wp-caption,
+.content figure {
+  margin: 0 30px 30px 30px;
+}
+
+.content .caption,
+.content .wp-caption-text,
+.content figcaption {
+  font-size: 16px;
+  line-height: 24px;
+  font-style: italic;
+}
+
+.content code,
+.content pre {
+  white-space: pre-wrap;
+}
+
+.content blockquote {
+  padding: 0;
+  -moz-padding-start: 16px;
+}
+
+.light > .content blockquote,
+.print > .content blockquote {
+  -moz-border-start: 2px solid #333333;
+}
+
+.dark > .content blockquote {
+  -moz-border-start: 2px solid #eeeeee;
+}
+
+.content ul,
+.content ol {
+  padding: 0;
+}
+
+.content ul {
+  -moz-padding-start: 30px;
+  list-style: disk;
+}
+
+.content ol {
+  -moz-padding-start: 30px;
+  list-style: decimal;
+}
+
+/* Toolbar */
+
+.toolbar {
+  display: none;
+}