merge m-c to fx-team
authorTim Taubert <ttaubert@mozilla.com>
Mon, 03 Mar 2014 09:30:26 +0100
changeset 171807 4cb766685b73ec0e22ad12d9eee8b78103555f6d
parent 171796 0085a162499fe86293833aa94eb6aba5f6e4fa75 (current diff)
parent 171806 c543d5d38e29f73137b1c788b0dbfa42e2e3a5dd (diff)
child 171808 73ab6437a1de56c631942a6dabb450207bfb5d8f
child 171874 8af0c466336ea079ba9c017519f263fb5d93994b
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
milestone30.0a1
merge m-c to fx-team
--- a/browser/base/content/sync/customize.xul
+++ b/browser/base/content/sync/customize.xul
@@ -33,28 +33,28 @@
     <description id="sync-customize-subtitle"
 #ifdef XP_UNIX
                  value="&syncCustomizeUnix.description;"
 #else
                  value="&syncCustomize.description;"
 #endif
                  />
 
+    <checkbox label="&engine.tabs.label;"
+              accesskey="&engine.tabs.accesskey;"
+              preference="engine.tabs"/>
     <checkbox label="&engine.bookmarks.label;"
               accesskey="&engine.bookmarks.accesskey;"
               preference="engine.bookmarks"/>
+    <checkbox label="&engine.passwords.label;"
+              accesskey="&engine.passwords.accesskey;"
+              preference="engine.passwords"/>
     <checkbox label="&engine.history.label;"
               accesskey="&engine.history.accesskey;"
               preference="engine.history"/>
-    <checkbox label="&engine.tabs.label;"
-              accesskey="&engine.tabs.accesskey;"
-              preference="engine.tabs"/>
-    <checkbox label="&engine.passwords.label;"
-              accesskey="&engine.passwords.accesskey;"
-              preference="engine.passwords"/>
     <checkbox label="&engine.addons.label;"
               accesskey="&engine.addons.accesskey;"
               preference="engine.addons"/>
     <checkbox label="&engine.prefs.label;"
               accesskey="&engine.prefs.accesskey;"
               preference="engine.prefs"/>
   </prefpane>
 
--- a/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js
+++ b/browser/components/customizableui/test/browser_946320_tabs_from_other_computers.js
@@ -89,36 +89,36 @@ function configureFxAccountIdentity() {
   let token = {
     endpoint: Weave.Svc.Prefs.get("tokenServerURI"),
     duration: 300,
     id: "id",
     key: "key",
     // uid will be set to the username.
   };
 
-  let MockInternal = {
-    signedInUser: {
-      version: DATA_FORMAT_VERSION,
-      accountData: user
-    },
-    getCertificate: function(data, keyPair, mustBeValidUntil) {
-      this.cert = {
-        validUntil: Date.now() + CERT_LIFETIME,
-        cert: "certificate",
-      };
-      return Promise.resolve(this.cert.cert);
-    },
-  };
-
+  let MockInternal = {};
   let mockTSC = { // TokenServerClient
     getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
       token.uid = "username";
       cb(null, token);
     },
   };
 
   let authService = Weave.Service.identity;
   authService._fxaService = new FxAccounts(MockInternal);
+
+  authService._fxaService.internal.currentAccountState.signedInUser = {
+    version: DATA_FORMAT_VERSION,
+    accountData: user
+  }
+  authService._fxaService.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+    this.cert = {
+      validUntil: authService._fxaService.internal.now() + CERT_LIFETIME,
+      cert: "certificate",
+    };
+    return Promise.resolve(this.cert.cert);
+  };
+
   authService._tokenServerClient = mockTSC;
   // Set the "account" of the browserId manager to be the "email" of the
   // logged in user of the mockFXA service.
   authService._account = user.email;
 }
--- a/browser/components/preferences/connection.js
+++ b/browser/components/preferences/connection.js
@@ -20,18 +20,18 @@ var gConnectionsDialog = {
     var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings");
     if (shareProxiesPref.value) {
       var proxyPrefs = ["ssl", "ftp", "socks"];
       for (var i = 0; i < proxyPrefs.length; ++i) {
         var proxyServerURLPref = document.getElementById("network.proxy." + proxyPrefs[i]);
         var proxyPortPref = document.getElementById("network.proxy." + proxyPrefs[i] + "_port");
         var backupServerURLPref = document.getElementById("network.proxy.backup." + proxyPrefs[i]);
         var backupPortPref = document.getElementById("network.proxy.backup." + proxyPrefs[i] + "_port");
-        backupServerURLPref.value = proxyServerURLPref.value;
-        backupPortPref.value = proxyPortPref.value;
+        backupServerURLPref.value = backupServerURLPref.value || proxyServerURLPref.value;
+        backupPortPref.value = backupPortPref.value || proxyPortPref.value;
         proxyServerURLPref.value = httpProxyURLPref.value;
         proxyPortPref.value = httpProxyPortPref.value;
       }
     }
     
     this.sanitizeNoProxiesPref();
     
     return true;
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -4,12 +4,13 @@ support-files =
   privacypane_tests_perwindow.js
 
 [browser_advanced_update.js]
 [browser_bug410900.js]
 [browser_bug731866.js]
 [browser_connection.js]
 [browser_healthreport.js]
 skip-if = !healthreport || (os == 'linux' && debug)
+[browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_5.js]
 [browser_privacypane_8.js]
--- a/browser/components/preferences/in-content/tests/browser_connection.js
+++ b/browser/components/preferences/in-content/tests/browser_connection.js
@@ -2,66 +2,63 @@
 /* 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/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 function test() {
   waitForExplicitFinish();
-  
+
   // network.proxy.type needs to be backed up and restored because mochitest
   // changes this setting from the default
   let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type");
   registerCleanupFunction(function() {
     Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType);
     Services.prefs.clearUserPref("network.proxy.no_proxies_on");
     Services.prefs.clearUserPref("browser.preferences.instantApply");
   });
-  
-  let connectionURI = "chrome://browser/content/preferences/connection.xul";
-  let windowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]
-                          .getService(Components.interfaces.nsIWindowWatcher);
+
+  let connectionURL = "chrome://browser/content/preferences/connection.xul";
+  let windowWatcher = Services.ww;
 
   // instantApply must be true, otherwise connection dialog won't save
   // when opened from in-content prefs
   Services.prefs.setBoolPref("browser.preferences.instantApply", true);
 
   // this observer is registered after the pref tab loads
   let observer = {
     observe: function(aSubject, aTopic, aData) {
-      
       if (aTopic == "domwindowopened") {
         // when connection window loads, run tests and acceptDialog()
         let win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
         win.addEventListener("load", function winLoadListener() {
           win.removeEventListener("load", winLoadListener, false);
-          if (win.location.href == connectionURI) {
+          if (win.location.href == connectionURL) {
             ok(true, "connection window opened");
             runConnectionTests(win);
             win.document.documentElement.acceptDialog();
           }
         }, false);
       } else if (aTopic == "domwindowclosed") {
         // finish up when connection window closes
         let win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
-        if (win.location.href == connectionURI) {
+        if (win.location.href == connectionURL) {
           windowWatcher.unregisterNotification(observer);
           ok(true, "connection window closed");
           // runConnectionTests will have changed this pref - make sure it was
           // sanitized correctly when the dialog was accepted
           is(Services.prefs.getCharPref("network.proxy.no_proxies_on"),
              ".a.com,.b.com,.c.com", "no_proxies_on pref has correct value");
           gBrowser.removeCurrentTab();
           finish();
         }
       }
-
     }
-  }
+  };
 
   /*
   The connection dialog alone won't save onaccept since it uses type="child",
   so it has to be opened as a sub dialog of the main pref tab.
   Open the main tab here.
   */
   open_preferences(function tabOpened(aContentWindow) {
     is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
@@ -77,17 +74,17 @@ function runConnectionTests(win) {
   let networkProxyNonePref = doc.getElementById("network.proxy.no_proxies_on");
   let networkProxyTypePref = doc.getElementById("network.proxy.type");
 
   // make sure the networkProxyNone textbox is formatted properly
   is(networkProxyNone.getAttribute("multiline"), "true",
      "networkProxyNone textbox is multiline");
   is(networkProxyNone.getAttribute("rows"), "2",
      "networkProxyNone textbox has two rows");
-  
+
   // check if sanitizing the given input for the no_proxies_on pref results in
   // expected string
   function testSanitize(input, expected, errorMessage) {
     networkProxyNonePref.value = input;
     win.gConnectionsDialog.sanitizeNoProxiesPref();
     is(networkProxyNonePref.value, expected, errorMessage);
   }
 
copy from browser/components/preferences/in-content/tests/browser_connection.js
copy to browser/components/preferences/in-content/tests/browser_proxy_backup.js
--- a/browser/components/preferences/in-content/tests/browser_connection.js
+++ b/browser/components/preferences/in-content/tests/browser_proxy_backup.js
@@ -2,123 +2,87 @@
 /* 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/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 function test() {
   waitForExplicitFinish();
-  
+
   // network.proxy.type needs to be backed up and restored because mochitest
   // changes this setting from the default
   let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type");
   registerCleanupFunction(function() {
     Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType);
-    Services.prefs.clearUserPref("network.proxy.no_proxies_on");
     Services.prefs.clearUserPref("browser.preferences.instantApply");
+    Services.prefs.clearUserPref("network.proxy.share_proxy_settings");
+    for (let proxyType of ["http", "ssl", "ftp", "socks"]) {
+      Services.prefs.clearUserPref("network.proxy." + proxyType);
+      Services.prefs.clearUserPref("network.proxy." + proxyType + "_port");
+      if (proxyType == "http") {
+        continue;
+      }
+      Services.prefs.clearUserPref("network.proxy.backup." + proxyType);
+      Services.prefs.clearUserPref("network.proxy.backup." + proxyType + "_port");
+    }
   });
-  
-  let connectionURI = "chrome://browser/content/preferences/connection.xul";
-  let windowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]
-                          .getService(Components.interfaces.nsIWindowWatcher);
+
+  let connectionURL = "chrome://browser/content/preferences/connection.xul";
+  let windowWatcher = Services.ww;
 
   // instantApply must be true, otherwise connection dialog won't save
   // when opened from in-content prefs
   Services.prefs.setBoolPref("browser.preferences.instantApply", true);
 
+  // Set a shared proxy and a SOCKS backup
+  Services.prefs.setIntPref("network.proxy.type", 1);
+  Services.prefs.setBoolPref("network.proxy.share_proxy_settings", true);
+  Services.prefs.setCharPref("network.proxy.http", "example.com");
+  Services.prefs.setIntPref("network.proxy.http_port", 1200);
+  Services.prefs.setCharPref("network.proxy.socks", "example.com");
+  Services.prefs.setIntPref("network.proxy.socks_port", 1200);
+  Services.prefs.setCharPref("network.proxy.backup.socks", "127.0.0.1");
+  Services.prefs.setIntPref("network.proxy.backup.socks_port", 9050);
+
   // this observer is registered after the pref tab loads
   let observer = {
     observe: function(aSubject, aTopic, aData) {
-      
       if (aTopic == "domwindowopened") {
         // when connection window loads, run tests and acceptDialog()
         let win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
         win.addEventListener("load", function winLoadListener() {
           win.removeEventListener("load", winLoadListener, false);
-          if (win.location.href == connectionURI) {
+          if (win.location.href == connectionURL) {
             ok(true, "connection window opened");
-            runConnectionTests(win);
             win.document.documentElement.acceptDialog();
           }
         }, false);
       } else if (aTopic == "domwindowclosed") {
         // finish up when connection window closes
         let win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
-        if (win.location.href == connectionURI) {
+        if (win.location.href == connectionURL) {
           windowWatcher.unregisterNotification(observer);
           ok(true, "connection window closed");
-          // runConnectionTests will have changed this pref - make sure it was
-          // sanitized correctly when the dialog was accepted
-          is(Services.prefs.getCharPref("network.proxy.no_proxies_on"),
-             ".a.com,.b.com,.c.com", "no_proxies_on pref has correct value");
+
+          // The SOCKS backup should not be replaced by the shared value
+          is(Services.prefs.getCharPref("network.proxy.backup.socks"), "127.0.0.1", "Shared proxy backup shouldn't be replaced");
+          is(Services.prefs.getIntPref("network.proxy.backup.socks_port"), 9050, "Shared proxy port backup shouldn't be replaced");
+
           gBrowser.removeCurrentTab();
           finish();
         }
       }
-
     }
-  }
+  };
 
   /*
   The connection dialog alone won't save onaccept since it uses type="child",
   so it has to be opened as a sub dialog of the main pref tab.
   Open the main tab here.
   */
   open_preferences(function tabOpened(aContentWindow) {
     is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
     windowWatcher.registerNotification(observer);
     gBrowser.contentWindow.gAdvancedPane.showConnections();
   });
 }
 
-// run a bunch of tests on the window containing connection.xul
-function runConnectionTests(win) {
-  let doc = win.document;
-  let networkProxyNone = doc.getElementById("networkProxyNone");
-  let networkProxyNonePref = doc.getElementById("network.proxy.no_proxies_on");
-  let networkProxyTypePref = doc.getElementById("network.proxy.type");
-
-  // make sure the networkProxyNone textbox is formatted properly
-  is(networkProxyNone.getAttribute("multiline"), "true",
-     "networkProxyNone textbox is multiline");
-  is(networkProxyNone.getAttribute("rows"), "2",
-     "networkProxyNone textbox has two rows");
-  
-  // check if sanitizing the given input for the no_proxies_on pref results in
-  // expected string
-  function testSanitize(input, expected, errorMessage) {
-    networkProxyNonePref.value = input;
-    win.gConnectionsDialog.sanitizeNoProxiesPref();
-    is(networkProxyNonePref.value, expected, errorMessage);
-  }
-
-  // change this pref so proxy exceptions are actually configurable
-  networkProxyTypePref.value = 1;
-  is(networkProxyNone.disabled, false, "networkProxyNone textbox is enabled");
-
-  testSanitize(".a.com", ".a.com",
-               "sanitize doesn't mess up single filter");
-  testSanitize(".a.com, .b.com, .c.com", ".a.com, .b.com, .c.com",
-               "sanitize doesn't mess up multiple comma/space sep filters");
-  testSanitize(".a.com\n.b.com", ".a.com,.b.com",
-               "sanitize turns line break into comma");
-  testSanitize(".a.com,\n.b.com", ".a.com,.b.com",
-               "sanitize doesn't add duplicate comma after comma");
-  testSanitize(".a.com\n,.b.com", ".a.com,.b.com",
-               "sanitize doesn't add duplicate comma before comma");
-  testSanitize(".a.com,\n,.b.com", ".a.com,,.b.com",
-               "sanitize doesn't add duplicate comma surrounded by commas");
-  testSanitize(".a.com, \n.b.com", ".a.com, .b.com",
-               "sanitize doesn't add comma after comma/space");
-  testSanitize(".a.com\n .b.com", ".a.com, .b.com",
-               "sanitize adds comma before space");
-  testSanitize(".a.com\n\n\n;;\n;\n.b.com", ".a.com,.b.com",
-               "sanitize only adds one comma per substring of bad chars");
-  testSanitize(".a.com,,.b.com", ".a.com,,.b.com",
-               "duplicate commas from user are untouched");
-  testSanitize(".a.com\n.b.com\n.c.com,\n.d.com,\n.e.com",
-               ".a.com,.b.com,.c.com,.d.com,.e.com",
-               "sanitize replaces things globally");
-
-  // will check that this was sanitized properly after window closes
-  networkProxyNonePref.value = ".a.com;.b.com\n.c.com";
-}
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -259,44 +259,44 @@
 
           <groupbox id="syncOptions">
             <caption label="&syncBrand.shortName.label;"/>
             <vbox>
               <richlistbox id="fxaSyncEnginesList"
                            orient="vertical"
                            onselect="if (this.selectedCount) this.clearSelection();">
                 <richlistitem>
-                  <checkbox label="&engine.addons.label;"
-                            accesskey="&engine.addons.accesskey;"
-                            preference="engine.addons"/>
+                  <checkbox label="&engine.tabs.label;"
+                            accesskey="&engine.tabs.accesskey;"
+                            preference="engine.tabs"/>
                 </richlistitem>
                 <richlistitem>
                   <checkbox label="&engine.bookmarks.label;"
                             accesskey="&engine.bookmarks.accesskey;"
                             preference="engine.bookmarks"/>
                 </richlistitem>
                 <richlistitem>
                   <checkbox label="&engine.passwords.label;"
                             accesskey="&engine.passwords.accesskey;"
                             preference="engine.passwords"/>
                 </richlistitem>
                 <richlistitem>
-                  <checkbox label="&engine.prefs.label;"
-                            accesskey="&engine.prefs.accesskey;"
-                            preference="engine.prefs"/>
-                </richlistitem>
-                <richlistitem>
                   <checkbox label="&engine.history.label;"
                             accesskey="&engine.history.accesskey;"
                             preference="engine.history"/>
                 </richlistitem>
                 <richlistitem>
-                  <checkbox label="&engine.tabs.label;"
-                            accesskey="&engine.tabs.accesskey;"
-                            preference="engine.tabs"/>
+                  <checkbox label="&engine.addons.label;"
+                            accesskey="&engine.addons.accesskey;"
+                            preference="engine.addons"/>
+                </richlistitem>
+                <richlistitem>
+                  <checkbox label="&engine.prefs.label;"
+                            accesskey="&engine.prefs.accesskey;"
+                            preference="engine.prefs"/>
                 </richlistitem>
               </richlistbox>
             </vbox>
           </groupbox>
           <hbox align="center">
             <label value="&syncDeviceName.label;"
                    accesskey="&syncDeviceName.accesskey;"
                    control="syncComputerName"/>
--- a/browser/locales/en-US/chrome/browser/preferences/connection.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/connection.dtd
@@ -40,10 +40,10 @@
 <!ENTITY  FTPport.accesskey             "r">
 <!ENTITY  SOCKSport.accesskey           "t">
 <!ENTITY  noproxy.label                 "No Proxy for:">
 <!ENTITY  noproxy.accesskey             "n">
 <!ENTITY  noproxyExplain.label          "Example: .mozilla.org, .net.nz, 192.168.1.0/24">
 <!ENTITY  shareproxy.label              "Use this proxy server for all protocols">
 <!ENTITY  shareproxy.accesskey          "s">
 <!ENTITY  autologinproxy.label          "Do not prompt for authentication if password is saved">
-<!ENTITY  autologinproxy.accesskey      "v">
+<!ENTITY  autologinproxy.accesskey      "i">
 <!ENTITY  autologinproxy.tooltip        "This option silently authenticates you to proxies when you have saved credentials for them. You will be prompted if authentication fails.">
--- a/browser/metro/base/content/bindings/urlbar.xml
+++ b/browser/metro/base/content/bindings/urlbar.xml
@@ -729,16 +729,31 @@
               let value = controller.getValueAt(i);
               let label = controller.getCommentAt(i) || value;
               let iconURI = controller.getImageAt(i);
 
               item.setAttribute("autocomplete", true);
               item.setAttribute("label", label);
               item.setAttribute("value", value);
               item.setAttribute("iconURI", iconURI);
+              let xpFaviconURI = Services.io.newURI(iconURI.replace("moz-anno:favicon:",""), null, null);
+              Task.spawn(function() {
+                let colorInfo = yield ColorUtils.getForegroundAndBackgroundIconColors(xpFaviconURI);
+                if ( !(colorInfo && colorInfo.background && colorInfo.foreground) 
+                     || (item.getAttribute("iconURI") != iconURI) ) {
+                  return;
+                }
+                let { background, foreground } = colorInfo;
+                item.style.color = foreground; //color text
+                item.setAttribute("customColor", background);
+                // when bound, use the setter to propogate the color change through the tile
+                if ('color' in item) {
+                  item.color = background;
+                }
+              }).then(null, err => Components.utils.reportError(err));
             }
 
             this._results.arrangeItems();
           ]]>
         </body>
       </method>
 
       <!-- Updating grid content: search engines -->
--- a/browser/metro/base/content/browser-scripts.js
+++ b/browser/metro/base/content/browser-scripts.js
@@ -35,16 +35,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/DownloadUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ColorUtils",
+                                  "resource:///modules/colorUtils.jsm");
+
 #ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
                                   "resource://shumway/ShumwayUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PdfJs",
                                   "resource://pdf.js/PdfJs.jsm");
 #endif
 
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -245,17 +245,23 @@
 
   /* Glass Fog */
 
   #TabsToolbar:not(:-moz-lwtheme) {
     background-image: none;
     position: relative;
   }
 
-  #TabsToolbar:not(:-moz-lwtheme)::before {
+  #TabsToolbar:not(:-moz-lwtheme)::after {
+    /* Because we use placeholders for window controls etc. in the tabstrip,
+     * and position those with ordinal attributes, and because our layout code
+     * expects :before/:after nodes to come first/last in the frame list,
+     * we have to reorder this element to come last, hence the
+     * ordinal group value (see bug 853415). */
+    -moz-box-ordinal-group: 1001;
     box-shadow: 0 0 30px 30px rgba(174,189,204,0.85);
     content: "";
     display: -moz-box;
     height: 0;
     margin: 0 60px; /* (30px + 30px) from box-shadow */
     position: absolute;
     pointer-events: none;
     top: 50%;
--- a/services/common/tokenserverclient.js
+++ b/services/common/tokenserverclient.js
@@ -29,31 +29,40 @@ const Prefs = new Preferences("services.
  *        (string) Error message.
  */
 this.TokenServerClientError = function TokenServerClientError(message) {
   this.name = "TokenServerClientError";
   this.message = message || "Client error.";
 }
 TokenServerClientError.prototype = new Error();
 TokenServerClientError.prototype.constructor = TokenServerClientError;
+TokenServerClientError.prototype._toStringFields = function() {
+  return {message: this.message};
+}
+TokenServerClientError.prototype.toString = function() {
+  return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
+}
 
 /**
  * Represents a TokenServerClient error that occurred in the network layer.
  *
  * @param error
  *        The underlying error thrown by the network layer.
  */
 this.TokenServerClientNetworkError =
  function TokenServerClientNetworkError(error) {
   this.name = "TokenServerClientNetworkError";
   this.error = error;
 }
 TokenServerClientNetworkError.prototype = new TokenServerClientError();
 TokenServerClientNetworkError.prototype.constructor =
   TokenServerClientNetworkError;
+TokenServerClientNetworkError.prototype._toStringFields = function() {
+  return {error: this.error};
+}
 
 /**
  * Represents a TokenServerClient error that occurred on the server.
  *
  * This type will be encountered for all non-200 response codes from the
  * server. The type of error is strongly enumerated and is stored in the
  * `cause` property. This property can have the following string values:
  *
@@ -77,24 +86,39 @@ TokenServerClientNetworkError.prototype.
  *   general -- A general server error has occurred. Clients should
  *     interpret this as an opaque failure.
  *
  * @param message
  *        (string) Error message.
  */
 this.TokenServerClientServerError =
  function TokenServerClientServerError(message, cause="general") {
+  this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues.
   this.name = "TokenServerClientServerError";
   this.message = message || "Server error.";
   this.cause = cause;
 }
 TokenServerClientServerError.prototype = new TokenServerClientError();
 TokenServerClientServerError.prototype.constructor =
   TokenServerClientServerError;
 
+TokenServerClientServerError.prototype._toStringFields = function() {
+  let fields = {
+    now: this.now,
+    message: this.message,
+    cause: this.cause,
+  };
+  if (this.response) {
+    fields.response_body = this.response.body;
+    fields.response_headers = this.response.headers;
+    fields.response_status = this.response.status;
+  }
+  return fields;
+};
+
 /**
  * Represents a client to the Token Server.
  *
  * http://docs.services.mozilla.com/token/index.html
  *
  * The Token Server supports obtaining tokens for arbitrary apps by
  * constructing URI paths of the form <app>/<app_version>. However, the service
  * discovery mechanism emphasizes the use of full URIs and tries to not force
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -36,16 +36,174 @@ let publicProperties = [
   "promiseAccountsForceSigninURI",
   "resendVerificationEmail",
   "setSignedInUser",
   "signOut",
   "version",
   "whenVerified"
 ];
 
+// An AccountState object holds all state related to one specific account.
+// Only one AccountState is ever "current" in the FxAccountsInternal object -
+// whenever a user logs out or logs in, the current AccountState is discarded,
+// making it impossible for the wrong state or state data to be accidentally
+// used.
+// In addition, it has some promise-related helpers to ensure that if an
+// attempt is made to resolve a promise on a "stale" state (eg, if an
+// operation starts, but a different user logs in before the operation
+// completes), the promise will be rejected.
+// It is intended to be used thusly:
+// somePromiseBasedFunction: function() {
+//   let currentState = this.currentAccountState;
+//   return someOtherPromiseFunction().then(
+//     data => currentState.resolve(data)
+//   );
+// }
+// If the state has changed between the function being called and the promise
+// being resolved, the .resolve() call will actually be rejected.
+AccountState = function(fxaInternal) {
+  this.fxaInternal = fxaInternal;
+};
+
+AccountState.prototype = {
+  cert: null,
+  keyPair: null,
+  signedInUser: null,
+  whenVerifiedPromise: null,
+  whenKeysReadyPromise: null,
+
+  get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
+
+  abort: function() {
+    if (this.whenVerifiedPromise) {
+      this.whenVerifiedPromise.reject(
+        new Error("Verification aborted; Another user signing in"));
+      this.whenVerifiedPromise = null;
+    }
+
+    if (this.whenKeysReadyPromise) {
+      this.whenKeysReadyPromise.reject(
+        new Error("Verification aborted; Another user signing in"));
+      this.whenKeysReadyPromise = null;
+    }
+    this.cert = null;
+    this.keyPair = null;
+    this.signedInUser = null;
+    this.fxaInternal = null;
+  },
+
+  getUserAccountData: function() {
+    // Skip disk if user is cached.
+    if (this.signedInUser) {
+      return this.resolve(this.signedInUser.accountData);
+    }
+
+    return this.fxaInternal.signedInUserStorage.get().then(
+      user => {
+        log.debug("getUserAccountData -> " + JSON.stringify(user));
+        if (user && user.version == this.version) {
+          log.debug("setting signed in user");
+          this.signedInUser = user;
+        }
+        return this.resolve(user ? user.accountData : null);
+      },
+      err => {
+        if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
+          // File hasn't been created yet.  That will be done
+          // on the first call to getSignedInUser
+          return this.resolve(null);
+        }
+        return this.reject(err);
+      }
+    );
+  },
+
+  setUserAccountData: function(accountData) {
+    return this.fxaInternal.signedInUserStorage.get().then(record => {
+      if (!this.isCurrent) {
+        return this.reject(new Error("Another user has signed in"));
+      }
+      record.accountData = accountData;
+      this.signedInUser = record;
+      return this.fxaInternal.signedInUserStorage.set(record)
+        .then(() => this.resolve(accountData));
+    });
+  },
+
+
+  getCertificate: function(data, keyPair, mustBeValidUntil) {
+    log.debug("getCertificate" + JSON.stringify(this.signedInUser));
+    // TODO: get the lifetime from the cert's .exp field
+    if (this.cert && this.cert.validUntil > mustBeValidUntil) {
+      log.debug(" getCertificate already had one");
+      return this.resolve(this.cert.cert);
+    }
+    // else get our cert signed
+    let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME;
+    return this.fxaInternal.getCertificateSigned(data.sessionToken,
+                                                 keyPair.serializedPublicKey,
+                                                 CERT_LIFETIME).then(
+      cert => {
+        this.cert = {
+          cert: cert,
+          validUntil: willBeValidUntil
+        };
+        return cert;
+      }
+    ).then(result => this.resolve(result));
+  },
+
+  getKeyPair: function(mustBeValidUntil) {
+    if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
+      log.debug("getKeyPair: already have a keyPair");
+      return this.resolve(this.keyPair.keyPair);
+    }
+    // Otherwse, create a keypair and set validity limit.
+    let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME;
+    let d = Promise.defer();
+    jwcrypto.generateKeyPair("DS160", (err, kp) => {
+      if (err) {
+        return this.reject(err);
+      }
+      this.keyPair = {
+        keyPair: kp,
+        validUntil: willBeValidUntil
+      };
+      log.debug("got keyPair");
+      delete this.cert;
+      d.resolve(this.keyPair.keyPair);
+    });
+    return d.promise.then(result => this.resolve(result));
+  },
+
+  resolve: function(result) {
+    if (!this.isCurrent) {
+      log.info("An accountState promise was resolved, but was actually rejected" +
+               " due to a different user being signed in. Originally resolved" +
+               " with: " + result);
+      return Promise.reject(new Error("A different user signed in"));
+    }
+    return Promise.resolve(result);
+  },
+
+  reject: function(error) {
+    // It could be argued that we should just let it reject with the original
+    // error - but this runs the risk of the error being (eg) a 401, which
+    // might cause the consumer to attempt some remediation and cause other
+    // problems.
+    if (!this.isCurrent) {
+      log.info("An accountState promise was rejected, but we are ignoring that" +
+               "reason and rejecting it due to a different user being signed in." +
+               "Originally rejected with: " + reason);
+      return Promise.reject(new Error("A different user signed in"));
+    }
+    return Promise.reject(error);
+  },
+
+}
 /**
  * The public API's constructor.
  */
 this.FxAccounts = function (mockInternal) {
   let internal = new FxAccountsInternal();
   let external = {};
 
   // Copy all public properties to the 'external' object.
@@ -65,40 +223,36 @@ this.FxAccounts = function (mockInternal
 
   return Object.freeze(external);
 }
 
 /**
  * The internal API's constructor.
  */
 function FxAccountsInternal() {
-  this.cert = null;
-  this.keyPair = null;
-  this.signedInUser = null;
   this.version = DATA_FORMAT_VERSION;
 
   // Make a local copy of these constants so we can mock it in testing
   this.POLL_STEP = POLL_STEP;
   this.POLL_SESSION = POLL_SESSION;
   // We will create this.pollTimeRemaining below; it will initially be
   // set to the value of POLL_SESSION.
 
   // We interact with the Firefox Accounts auth server in order to confirm that
   // a user's email has been verified and also to fetch the user's keys from
   // the server.  We manage these processes in possibly long-lived promises
   // that are internal to this object (never exposed to callers).  Because
   // Firefox Accounts allows for only one logged-in user, and because it's
   // conceivable that while we are waiting to verify one identity, a caller
   // could start verification on a second, different identity, we need to be
   // able to abort all work on the first sign-in process.  The currentTimer and
-  // generationCount are used for this purpose.
-  this.whenVerifiedPromise = null;
-  this.whenKeysReadyPromise = null;
+  // currentAccountState are used for this purpose.
+  // (XXX - should the timer be directly on the currentAccountState?)
   this.currentTimer = null;
-  this.generationCount = 0;
+  this.currentAccountState = new AccountState(this);
 
   this.fxAccountsClient = new FxAccountsClient();
 
   // We don't reference |profileDir| in the top-level module scope
   // as we may be imported before we know where it is.
   this.signedInUserStorage = new JSONStorage({
     filename: DEFAULT_STORAGE_FILENAME,
     baseDir: OS.Constants.Path.profileDir,
@@ -179,28 +333,29 @@ FxAccountsInternal.prototype = {
    *          kB: An encryption key derived from the user's FxA password
    *          verified: email verification status
    *          authAt: The time (seconds since epoch) that this record was
    *                  authenticated
    *        }
    *        or null if no user is signed in.
    */
   getSignedInUser: function getSignedInUser() {
-    return this.getUserAccountData().then(data => {
+    let currentState = this.currentAccountState;
+    return currentState.getUserAccountData().then(data => {
       if (!data) {
         return null;
       }
       if (!this.isUserEmailVerified(data)) {
         // If the email is not verified, start polling for verification,
         // but return null right away.  We don't want to return a promise
         // that might not be fulfilled for a long time.
         this.startVerifiedCheck(data);
       }
       return data;
-    });
+    }).then(result => currentState.resolve(result));
   },
 
   /**
    * Set the current user signed in to Firefox Accounts.
    *
    * @param credentials
    *        The credentials object obtained by logging in or creating
    *        an account on the FxA server:
@@ -217,99 +372,90 @@ FxAccountsInternal.prototype = {
    *         The promise resolves to null when the data is saved
    *         successfully and is rejected on error.
    */
   setSignedInUser: function setSignedInUser(credentials) {
     log.debug("setSignedInUser - aborting any existing flows");
     this.abortExistingFlow();
 
     let record = {version: this.version, accountData: credentials};
+    let currentState = this.currentAccountState;
     // Cache a clone of the credentials object.
-    this.signedInUser = JSON.parse(JSON.stringify(record));
+    currentState.signedInUser = JSON.parse(JSON.stringify(record));
 
     // This promise waits for storage, but not for verification.
     // We're telling the caller that this is durable now.
     return this.signedInUserStorage.set(record).then(() => {
       this.notifyObservers(ONLOGIN_NOTIFICATION);
       if (!this.isUserEmailVerified(credentials)) {
         this.startVerifiedCheck(credentials);
       }
-    });
+    }).then(result => currentState.resolve(result));
   },
 
   /**
    * returns a promise that fires with the assertion.  If there is no verified
    * signed-in user, fires with null.
    */
   getAssertion: function getAssertion(audience) {
     log.debug("enter getAssertion()");
+    let currentState = this.currentAccountState;
     let mustBeValidUntil = this.now() + ASSERTION_LIFETIME;
-    return this.getUserAccountData().then(data => {
+    return currentState.getUserAccountData().then(data => {
       if (!data) {
         // No signed-in user
         return null;
       }
       if (!this.isUserEmailVerified(data)) {
         // Signed-in user has not verified email
         return null;
       }
-      return this.getKeyPair(mustBeValidUntil).then(keyPair => {
-        return this.getCertificate(data, keyPair, mustBeValidUntil)
+      return currentState.getKeyPair(mustBeValidUntil).then(keyPair => {
+        return currentState.getCertificate(data, keyPair, mustBeValidUntil)
           .then(cert => {
             return this.getAssertionFromCert(data, keyPair, cert, audience);
           });
       });
-    });
+    }).then(result => currentState.resolve(result));
   },
 
   /**
    * Resend the verification email fot the currently signed-in user.
    *
    */
   resendVerificationEmail: function resendVerificationEmail() {
+    let currentState = this.currentAccountState;
     return this.getSignedInUser().then(data => {
       // If the caller is asking for verification to be re-sent, and there is
       // no signed-in user to begin with, this is probably best regarded as an
       // error.
       if (data) {
-        this.pollEmailStatus(data.sessionToken, "start");
+        this.pollEmailStatus(currentState, data.sessionToken, "start");
         return this.fxAccountsClient.resendVerificationEmail(data.sessionToken);
       }
       throw new Error("Cannot resend verification email; no signed-in user");
     });
   },
 
   /*
    * Reset state such that any previous flow is canceled.
    */
   abortExistingFlow: function abortExistingFlow() {
     if (this.currentTimer) {
       log.debug("Polling aborted; Another user signing in");
       clearTimeout(this.currentTimer);
       this.currentTimer = 0;
     }
-    this.generationCount++;
-    log.debug("generationCount: " + this.generationCount);
-
-    if (this.whenVerifiedPromise) {
-      this.whenVerifiedPromise.reject(
-        new Error("Verification aborted; Another user signing in"));
-      this.whenVerifiedPromise = null;
-    }
-
-    if (this.whenKeysReadyPromise) {
-      this.whenKeysReadyPromise.reject(
-        new Error("KeyFetch aborted; Another user signing in"));
-      this.whenKeysReadyPromise = null;
-    }
+    this.currentAccountState.abort();
+    this.currentAccountState = new AccountState(this);
   },
 
   signOut: function signOut() {
     this.abortExistingFlow();
-    this.signedInUser = null; // clear in-memory cache
+    this.currentAccountState.signedInUser = null; // clear in-memory cache
     return this.signedInUserStorage.set(null).then(() => {
       this.notifyObservers(ONLOGOUT_NOTIFICATION);
     });
   },
 
   /**
    * Fetch encryption keys for the signed-in-user from the FxA API server.
    *
@@ -325,48 +471,47 @@ FxAccountsInternal.prototype = {
    *          sessionToken: Session for the FxA server
    *          kA: An encryption key from the FxA server
    *          kB: An encryption key derived from the user's FxA password
    *          verified: email verification status
    *        }
    *        or null if no user is signed in
    */
   getKeys: function() {
-    return this.getUserAccountData().then((data) => {
+    let currentState = this.currentAccountState;
+    return currentState.getUserAccountData().then((data) => {
       if (!data) {
         throw new Error("Can't get keys; User is not signed in");
       }
       if (data.kA && data.kB) {
         return data;
       }
-      if (!this.whenKeysReadyPromise) {
-        this.whenKeysReadyPromise = Promise.defer();
+      if (!currentState.whenKeysReadyPromise) {
+        currentState.whenKeysReadyPromise = Promise.defer();
         this.fetchAndUnwrapKeys(data.keyFetchToken).then(data => {
-          if (this.whenKeysReadyPromise) {
-            this.whenKeysReadyPromise.resolve(data);
-          }
+          currentState.whenKeysReadyPromise.resolve(data);
         });
       }
-      return this.whenKeysReadyPromise.promise;
-    });
+      return currentState.whenKeysReadyPromise.promise;
+    }).then(result => currentState.resolve(result));
    },
 
   fetchAndUnwrapKeys: function(keyFetchToken) {
     log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
+    let currentState = this.currentAccountState;
     return Task.spawn(function* task() {
       // Sign out if we don't have a key fetch token.
       if (!keyFetchToken) {
         yield this.signOut();
         return null;
       }
-      let myGenerationCount = this.generationCount;
 
       let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken);
 
-      let data = yield this.getUserAccountData();
+      let data = yield currentState.getUserAccountData();
 
       // Sanity check that the user hasn't changed out from under us
       if (data.keyFetchToken !== keyFetchToken) {
         throw new Error("Signed in user changed while fetching keys!");
       }
 
       // Next statements must be synchronous until we setUserAccountData
       // so that we don't risk getting into a weird state.
@@ -376,251 +521,162 @@ FxAccountsInternal.prototype = {
       log.debug("kB_hex: " + kB_hex);
       data.kA = CommonUtils.bytesAsHex(kA);
       data.kB = CommonUtils.bytesAsHex(kB_hex);
 
       delete data.keyFetchToken;
 
       log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
 
-      // Before writing any data, ensure that a new flow hasn't been
-      // started behind our backs.
-      if (this.generationCount !== myGenerationCount) {
-        return null;
-      }
-
-      yield this.setUserAccountData(data);
-
+      yield currentState.setUserAccountData(data);
       // We are now ready for business. This should only be invoked once
       // per setSignedInUser(), regardless of whether we've rebooted since
       // setSignedInUser() was called.
       this.notifyObservers(ONVERIFIED_NOTIFICATION);
       return data;
-    }.bind(this));
+    }.bind(this)).then(result => currentState.resolve(result));
   },
 
   getAssertionFromCert: function(data, keyPair, cert, audience) {
     log.debug("getAssertionFromCert");
     let payload = {};
     let d = Promise.defer();
     let options = {
       localtimeOffsetMsec: this.localtimeOffsetMsec,
       now: this.now()
     };
+    let currentState = this.currentAccountState;
     // "audience" should look like "http://123done.org".
     // The generated assertion will expire in two minutes.
     jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
       if (err) {
         log.error("getAssertionFromCert: " + err);
         d.reject(err);
       } else {
         log.debug("getAssertionFromCert returning signed: " + signed);
         d.resolve(signed);
       }
     });
-    return d.promise;
-  },
-
-  getCertificate: function(data, keyPair, mustBeValidUntil) {
-    log.debug("getCertificate" + JSON.stringify(this.signedInUserStorage));
-    // TODO: get the lifetime from the cert's .exp field
-    if (this.cert && this.cert.validUntil > mustBeValidUntil) {
-      log.debug(" getCertificate already had one");
-      return Promise.resolve(this.cert.cert);
-    }
-    // else get our cert signed
-    let willBeValidUntil = this.now() + CERT_LIFETIME;
-    return this.getCertificateSigned(data.sessionToken,
-                                     keyPair.serializedPublicKey,
-                                     CERT_LIFETIME)
-      .then((cert) => {
-        this.cert = {
-          cert: cert,
-          validUntil: willBeValidUntil
-        };
-        return cert;
-      }
-    );
+    return d.promise.then(result => currentState.resolve(result));
   },
 
   getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) {
     log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey);
-    return this.fxAccountsClient.signCertificate(sessionToken,
-                                                 JSON.parse(serializedPublicKey),
-                                                 lifetime);
-  },
-
-  getKeyPair: function(mustBeValidUntil) {
-    if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
-      log.debug("getKeyPair: already have a keyPair");
-      return Promise.resolve(this.keyPair.keyPair);
-    }
-    // Otherwse, create a keypair and set validity limit.
-    let willBeValidUntil = this.now() + KEY_LIFETIME;
-    let d = Promise.defer();
-    jwcrypto.generateKeyPair("DS160", (err, kp) => {
-      if (err) {
-        d.reject(err);
-      } else {
-        this.keyPair = {
-          keyPair: kp,
-          validUntil: willBeValidUntil
-        };
-        log.debug("got keyPair");
-        delete this.cert;
-        d.resolve(this.keyPair.keyPair);
-      }
-    });
-    return d.promise;
+    return this.fxAccountsClient.signCertificate(
+      sessionToken,
+      JSON.parse(serializedPublicKey),
+      lifetime
+    );
   },
 
   getUserAccountData: function() {
-    // Skip disk if user is cached.
-    if (this.signedInUser) {
-      return Promise.resolve(this.signedInUser.accountData);
-    }
-
-    let deferred = Promise.defer();
-    this.signedInUserStorage.get()
-      .then((user) => {
-        log.debug("getUserAccountData -> " + JSON.stringify(user));
-        if (user && user.version == this.version) {
-          log.debug("setting signed in user");
-          this.signedInUser = user;
-        }
-        deferred.resolve(user ? user.accountData : null);
-      },
-      (err) => {
-        if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
-          // File hasn't been created yet.  That will be done
-          // on the first call to getSignedInUser
-          deferred.resolve(null);
-        } else {
-          deferred.reject(err);
-        }
-      }
-    );
-
-    return deferred.promise;
+    return this.currentAccountState.getUserAccountData();
   },
 
   isUserEmailVerified: function isUserEmailVerified(data) {
     return !!(data && data.verified);
   },
 
   /**
    * Setup for and if necessary do email verification polling.
    */
   loadAndPoll: function() {
-    return this.getUserAccountData()
+    let currentState = this.currentAccountState;
+    return currentState.getUserAccountData()
       .then(data => {
         if (data && !this.isUserEmailVerified(data)) {
-          this.pollEmailStatus(data.sessionToken, "start");
+          this.pollEmailStatus(currentState, data.sessionToken, "start");
         }
         return data;
       });
   },
 
   startVerifiedCheck: function(data) {
     log.debug("startVerifiedCheck " + JSON.stringify(data));
     // Get us to the verified state, then get the keys. This returns a promise
     // that will fire when we are completely ready.
     //
     // Login is truly complete once keys have been fetched, so once getKeys()
     // obtains and stores kA and kB, it will fire the onverified observer
     // notification.
     return this.whenVerified(data)
-      .then((data) => this.getKeys(data));
+      .then(() => this.getKeys());
   },
 
   whenVerified: function(data) {
+    let currentState = this.currentAccountState;
     if (data.verified) {
       log.debug("already verified");
-      return Promise.resolve(data);
+      return currentState.resolve(data);
     }
-    if (!this.whenVerifiedPromise) {
+    if (!currentState.whenVerifiedPromise) {
       log.debug("whenVerified promise starts polling for verified email");
-      this.pollEmailStatus(data.sessionToken, "start");
+      this.pollEmailStatus(currentState, data.sessionToken, "start");
     }
-    return this.whenVerifiedPromise.promise;
+    return currentState.whenVerifiedPromise.promise.then(
+      result => currentState.resolve(result)
+    );
   },
 
   notifyObservers: function(topic) {
     log.debug("Notifying observers of " + topic);
     Services.obs.notifyObservers(null, topic, null);
   },
 
-  pollEmailStatus: function pollEmailStatus(sessionToken, why) {
-    let myGenerationCount = this.generationCount;
-    log.debug("entering pollEmailStatus: " + why + " " + myGenerationCount);
+  // XXX - pollEmailStatus should maybe be on the AccountState object?
+  pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) {
+    log.debug("entering pollEmailStatus: " + why);
     if (why == "start") {
       // If we were already polling, stop and start again.  This could happen
       // if the user requested the verification email to be resent while we
       // were already polling for receipt of an earlier email.
       this.pollTimeRemaining = this.POLL_SESSION;
-      if (!this.whenVerifiedPromise) {
-        this.whenVerifiedPromise = Promise.defer();
+      if (!currentState.whenVerifiedPromise) {
+        currentState.whenVerifiedPromise = Promise.defer();
       }
     }
 
     this.checkEmailStatus(sessionToken)
       .then((response) => {
         log.debug("checkEmailStatus -> " + JSON.stringify(response));
-        // Check to see if we're still current.
-        // If for some ghastly reason we are not, stop processing.
-        if (this.generationCount !== myGenerationCount) {
-          log.debug("generation count differs from " + this.generationCount + " - aborting");
-          log.debug("sessionToken on abort is " + sessionToken);
-          return;
-        }
-
         if (response && response.verified) {
           // Bug 947056 - Server should be able to tell FxAccounts.jsm to back
           // off or stop polling altogether
-          this.getUserAccountData()
+          currentState.getUserAccountData()
             .then((data) => {
               data.verified = true;
-              return this.setUserAccountData(data);
+              return currentState.setUserAccountData(data);
             })
             .then((data) => {
               // Now that the user is verified, we can proceed to fetch keys
-              if (this.whenVerifiedPromise) {
-                this.whenVerifiedPromise.resolve(data);
-                delete this.whenVerifiedPromise;
+              if (currentState.whenVerifiedPromise) {
+                currentState.whenVerifiedPromise.resolve(data);
+                delete currentState.whenVerifiedPromise;
               }
             });
         } else {
           log.debug("polling with step = " + this.POLL_STEP);
           this.pollTimeRemaining -= this.POLL_STEP;
           log.debug("time remaining: " + this.pollTimeRemaining);
           if (this.pollTimeRemaining > 0) {
             this.currentTimer = setTimeout(() => {
-              this.pollEmailStatus(sessionToken, "timer")}, this.POLL_STEP);
+              this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP);
             log.debug("started timer " + this.currentTimer);
           } else {
-            if (this.whenVerifiedPromise) {
-              this.whenVerifiedPromise.reject(
+            if (currentState.whenVerifiedPromise) {
+              currentState.whenVerifiedPromise.reject(
                 new Error("User email verification timed out.")
               );
-              delete this.whenVerifiedPromise;
+              delete currentState.whenVerifiedPromise;
             }
           }
         }
       });
     },
 
-  setUserAccountData: function(accountData) {
-    return this.signedInUserStorage.get().then(record => {
-      record.accountData = accountData;
-      this.signedInUser = record;
-      return this.signedInUserStorage.set(record)
-        .then(() => accountData);
-    });
-  },
-
   // Return the URI of the remote UI flows.
   getAccountsURI: function() {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.uri");
     if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     return url;
   },
@@ -636,25 +692,26 @@ FxAccountsInternal.prototype = {
 
   // Returns a promise that resolves with the URL to use to force a re-signin
   // of the current account.
   promiseAccountsForceSigninURI: function() {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri");
     if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
+    let currentState = this.currentAccountState;
     // but we need to append the email address onto a query string.
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
         return null;
       }
       let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
       newQueryPortion += "email=" + encodeURIComponent(accountData.email);
       return url + newQueryPortion;
-    });
+    }).then(result => currentState.resolve(result));
   }
 };
 
 /**
  * JSONStorage constructor that creates instances that may set/get
  * to a specified file, in a directory that will be created if it
  * doesn't exist.
  *
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -397,27 +397,29 @@ add_task(function test_getAssertion() {
   fxa.internal._d_signCertificate.resolve("cert1");
   let assertion = yield d;
   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
   do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken");
   do_check_neq(assertion, null);
   _("ASSERTION: " + assertion + "\n");
   let pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert1");
-  do_check_neq(fxa.internal.keyPair, undefined);
-  _(fxa.internal.keyPair.validUntil + "\n");
+  let keyPair = fxa.internal.currentAccountState.keyPair;
+  let cert = fxa.internal.currentAccountState.cert;
+  do_check_neq(keyPair, undefined);
+  _(keyPair.validUntil + "\n");
   let p2 = pieces[1].split(".");
   let header = JSON.parse(atob(p2[0]));
   _("HEADER: " + JSON.stringify(header) + "\n");
   do_check_eq(header.alg, "DS128");
   let payload = JSON.parse(atob(p2[1]));
   _("PAYLOAD: " + JSON.stringify(payload) + "\n");
   do_check_eq(payload.aud, "audience.example.com");
-  do_check_eq(fxa.internal.keyPair.validUntil, start + KEY_LIFETIME);
-  do_check_eq(fxa.internal.cert.validUntil, start + CERT_LIFETIME);
+  do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
+  do_check_eq(cert.validUntil, start + CERT_LIFETIME);
   _("delta: " + Date.parse(payload.exp - start) + "\n");
   let exp = Number(payload.exp);
 
   do_check_eq(exp, now + TWO_MINUTES_MS);
 
   // Reset for next call.
   fxa.internal._d_signCertificate = Promise.defer();
 
@@ -444,18 +446,20 @@ add_task(function test_getAssertion() {
   payload = JSON.parse(atob(p2[1]));
   do_check_eq(payload.aud, "third.example.com");
 
   // The keypair and cert should have the same validity as before, but the
   // expiration time of the assertion should be different.  We compare this to
   // the initial start time, to which they are relative, not the current value
   // of "now".
 
-  do_check_eq(fxa.internal.keyPair.validUntil, start + KEY_LIFETIME);
-  do_check_eq(fxa.internal.cert.validUntil, start + CERT_LIFETIME);
+  keyPair = fxa.internal.currentAccountState.keyPair;
+  cert = fxa.internal.currentAccountState.cert;
+  do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
+  do_check_eq(cert.validUntil, start + CERT_LIFETIME);
   exp = Number(payload.exp);
   do_check_eq(exp, now + TWO_MINUTES_MS);
 
   // Now we wait even longer, and expect both assertion and cert to expire.  So
   // we will have to get a new keypair and cert.
   now += ONE_DAY_MS;
   fxa.internal._now_is = now;
   d = fxa.getAssertion("fourth.example.com");
@@ -464,18 +468,20 @@ add_task(function test_getAssertion() {
   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2);
   do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken");
   pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert2");
   p2 = pieces[1].split(".");
   header = JSON.parse(atob(p2[0]));
   payload = JSON.parse(atob(p2[1]));
   do_check_eq(payload.aud, "fourth.example.com");
-  do_check_eq(fxa.internal.keyPair.validUntil, now + KEY_LIFETIME);
-  do_check_eq(fxa.internal.cert.validUntil, now + CERT_LIFETIME);
+  keyPair = fxa.internal.currentAccountState.keyPair;
+  cert = fxa.internal.currentAccountState.cert;
+  do_check_eq(keyPair.validUntil, now + KEY_LIFETIME);
+  do_check_eq(cert.validUntil, now + CERT_LIFETIME);
   exp = Number(payload.exp);
 
   do_check_eq(exp, now + TWO_MINUTES_MS);
   _("----- DONE ----\n");
 });
 
 add_task(function test_resend_email_not_signed_in() {
   let fxa = new MockFxAccounts();
@@ -489,40 +495,41 @@ add_task(function test_resend_email_not_
   }
   do_throw("Should not be able to resend email when nobody is signed in");
 });
 
 add_test(function test_resend_email() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
-  do_check_eq(fxa.internal.generationCount, 0);
+  let initialState = fxa.internal.currentAccountState;
 
   // Alice is the user signing in; her email is unverified.
   fxa.setSignedInUser(alice).then(() => {
     log.debug("Alice signing in");
 
     // We're polling for the first email
-    do_check_eq(fxa.internal.generationCount, 1);
+    do_check_true(fxa.internal.currentAccountState !== initialState);
+    let aliceState = fxa.internal.currentAccountState;
 
     // The polling timer is ticking
     do_check_true(fxa.internal.currentTimer > 0);
 
     fxa.internal.getUserAccountData().then(user => {
       do_check_eq(user.email, alice.email);
       do_check_eq(user.verified, false);
       log.debug("Alice wants verification email resent");
 
       fxa.resendVerificationEmail().then((result) => {
         // Mock server response; ensures that the session token actually was
         // passed to the client to make the hawk call
         do_check_eq(result, "alice's session token");
 
         // Timer was not restarted
-        do_check_eq(fxa.internal.generationCount, 1);
+        do_check_true(fxa.internal.currentAccountState === aliceState);
 
         // Timer is still ticking
         do_check_true(fxa.internal.currentTimer > 0);
 
         // Ok abort polling before we go on to the next test
         fxa.internal.abortExistingFlow();
         run_next_test();
       });
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -109,30 +109,30 @@ this.makeIdentityConfig = function(overr
   }
   return result;
 }
 
 // Configure an instance of an FxAccount identity provider with the specified
 // config (or the default config if not specified).
 this.configureFxAccountIdentity = function(authService,
                                            config = makeIdentityConfig()) {
-  let MockInternal = {
-    signedInUser: {
-      version: DATA_FORMAT_VERSION,
-      accountData: config.fxaccount.user
-    },
-    getCertificate: function(data, keyPair, mustBeValidUntil) {
-      this.cert = {
-        validUntil: Date.now() + CERT_LIFETIME,
-        cert: "certificate",
-      };
-      return Promise.resolve(this.cert.cert);
-    },
+  let MockInternal = {};
+  let fxa = new FxAccounts(MockInternal);
+
+  fxa.internal.currentAccountState.signedInUser = {
+    version: DATA_FORMAT_VERSION,
+    accountData: config.fxaccount.user
   };
-  let fxa = new FxAccounts(MockInternal);
+  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+    this.cert = {
+      validUntil: fxa.internal.now() + CERT_LIFETIME,
+      cert: "certificate",
+    };
+    return Promise.resolve(this.cert.cert);
+  };
 
   let mockTSC = { // TokenServerClient
     getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
       config.fxaccount.token.uid = config.username;
       cb(null, config.fxaccount.token);
     },
   };
   authService._fxaService = fxa;
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -50,21 +50,29 @@ function deriveKeyBundle(kB) {
   let bundle = new BulkKeyBundle();
   // [encryptionKey, hmacKey]
   bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
   return bundle;
 }
 
 /*
   General authentication error for abstracting authentication
-  errors from multiple sources (e.g., from FxAccounts, TokenServer)
-    'message' is a string with a description of the error
+  errors from multiple sources (e.g., from FxAccounts, TokenServer).
+  details is additional details about the error - it might be a string, or
+  some other error object (which should do the right thing when toString() is
+  called on it)
 */
-function AuthenticationError(message) {
-  this.message = message || "";
+function AuthenticationError(details) {
+  this.details = details;
+}
+
+AuthenticationError.prototype = {
+  toString: function() {
+    return "AuthenticationError(" + this.details + ")";
+  }
 }
 
 this.BrowserIDManager = function BrowserIDManager() {
   // NOTE: _fxaService and _tokenServerClient are replaced with mocks by
   // the test suite.
   this._fxaService = fxAccounts;
   this._tokenServerClient = new TokenServerClient();
   // will be a promise that resolves when we are ready to authenticate
@@ -157,21 +165,21 @@ this.BrowserIDManager.prototype = {
           Svc.Prefs.set("firstSync", "resetClient");
           Services.obs.notifyObservers(null, "weave:service:setup-complete", null);
           Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
         }
       }).then(null, err => {
         this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
         this.whenReadyToAuthenticate.reject(err);
         // report what failed...
-        this._log.error("Background fetch for key bundle failed: " + err.message);
+        this._log.error("Background fetch for key bundle failed: " + err);
       });
       // and we are done - the fetch continues on in the background...
     }).then(null, err => {
-      this._log.error("Processing logged in account: " + err.message);
+      this._log.error("Processing logged in account: " + err);
     });
   },
 
   observe: function (subject, topic, data) {
     this._log.debug("observed " + topic);
     switch (topic) {
     case fxAccountsCommon.ONLOGIN_NOTIFICATION:
       this.initializeWithCurrentIdentity(true);
@@ -420,17 +428,17 @@ this.BrowserIDManager.prototype = {
     log.info("Fetching assertion and token from: " + tokenServerURI);
 
     function getToken(tokenServerURI, assertion) {
       log.debug("Getting a token");
       let deferred = Promise.defer();
       let cb = function (err, token) {
         if (err) {
           log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err.message);
-          return deferred.reject(new AuthenticationError(err.message));
+          return deferred.reject(new AuthenticationError(err));
         } else {
           log.debug("Successfully got a sync token");
           return deferred.resolve(token);
         }
       };
 
       client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
       return deferred.promise;
@@ -460,17 +468,17 @@ this.BrowserIDManager.prototype = {
         token.expiration = this._now() + (token.duration * 1000) * 0.80;
         return token;
       })
       .then(null, err => {
         // TODO: write tests to make sure that different auth error cases are handled here
         // properly: auth error getting assertion, auth error getting token (invalid generation
         // and client-state error)
         if (err instanceof AuthenticationError) {
-          this._log.error("Authentication error in _fetchTokenForUser: " + err.message);
+          this._log.error("Authentication error in _fetchTokenForUser: " + err);
           // Drop the sync key bundle, but still expect to have one.
           // This will arrange for us to be in the right 'currentAuthState'
           // such that UI will show the right error.
           this._shouldHaveSyncKeyBundle = true;
           this._syncKeyBundle = null;
           Weave.Status.login = this.currentAuthState;
           Services.obs.notifyObservers(null, "weave:service:login:error", null);
         }
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -30,33 +30,33 @@ configureFxAccountIdentity(browseridMana
 let MockFxAccountsClient = function() {
   FxAccountsClient.apply(this);
 };
 MockFxAccountsClient.prototype = {
   __proto__: FxAccountsClient.prototype
 };
 
 function MockFxAccounts() {
-  return new FxAccounts({
+  let fxa = new FxAccounts({
     _now_is: Date.now(),
 
     now: function () {
       return this._now_is;
     },
 
-    getCertificate: function(data, keyPair, mustBeValidUntil) {
-      this.cert = {
-        validUntil: Date.now() + CERT_LIFETIME,
-        cert: "certificate",
-      };
-      return Promise.resolve(this.cert.cert);
-    },
-
     fxAccountsClient: new MockFxAccountsClient()
   });
+  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
+    this.cert = {
+      validUntil: fxa.internal.now() + CERT_LIFETIME,
+      cert: "certificate",
+    };
+    return Promise.resolve(this.cert.cert);
+  };
+  return fxa;
 }
 
 function run_test() {
   initTestLogging("Trace");
   Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
   run_next_test();
 };
 
@@ -142,17 +142,17 @@ add_test(function test_resourceAuthentic
 
   do_check_eq(fxa.now(), now);
   do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec);
 
   // Mocks within mocks...
   configureFxAccountIdentity(browseridManager, identityConfig);
 
   // Ensure the new FxAccounts mock has a signed-in user.
-  fxa.internal.signedInUser = browseridManager._fxaService.internal.signedInUser;
+  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
 
   browseridManager._fxaService = fxa;
 
   do_check_eq(browseridManager._fxaService.internal.now(), now);
   do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec,
       localtimeOffsetMsec);
 
   do_check_eq(browseridManager._fxaService.now(), now);
@@ -195,17 +195,17 @@ add_test(function test_RESTResourceAuthe
   fxaClient.hawk = hawkClient;
   let fxa = new MockFxAccounts();
   fxa.internal._now_is = now;
   fxa.internal.fxAccountsClient = fxaClient;
 
   configureFxAccountIdentity(browseridManager, identityConfig);
 
   // Ensure the new FxAccounts mock has a signed-in user.
-  fxa.internal.signedInUser = browseridManager._fxaService.internal.signedInUser;
+  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
 
   browseridManager._fxaService = fxa;
 
   do_check_eq(browseridManager._fxaService.internal.now(), now);
 
   let request = new SyncStorageRequest("https://example.net/i/like/pie/");
   let authenticator = browseridManager.getResourceAuthenticator();
   let output = authenticator(request, 'GET');
--- a/testing/mochitest/browser-test-overlay.xul
+++ b/testing/mochitest/browser-test-overlay.xul
@@ -4,9 +4,10 @@
    - 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/. -->
 
 <overlay id="browserTestOverlay"
          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
   <script type="application/javascript" src="chrome://mochikit/content/browser-test.js"/>
   <script type="application/javascript" src="chrome://mochikit/content/cc-analyzer.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/mochitest-e10s-utils.js"/>
 </overlay>
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -57,16 +57,19 @@ function testOnLoad() {
                          .getInterface(Components.interfaces.nsIWebNavigation);
       webNav.loadURI(url, null, null, null, null);
     };
 
     var listener = 'data:,function doLoad(e) { var data=e.getData("data");removeEventListener("contentEvent", function (e) { doLoad(e); }, false, true);sendAsyncMessage("chromeEvent", {"data":data}); };addEventListener("contentEvent", function (e) { doLoad(e); }, false, true);';
     messageManager.loadFrameScript(listener, true);
     messageManager.addMessageListener("chromeEvent", messageHandler);
   }
+  if (gConfig.e10s) {
+    e10s_init();
+  }
 }
 
 function Tester(aTests, aDumper, aCallback) {
   this.dumper = aDumper;
   this.tests = aTests;
   this.callback = aCallback;
   this.openedWindows = {};
   this.openedURLs = {};
--- a/testing/mochitest/jar.mn
+++ b/testing/mochitest/jar.mn
@@ -1,15 +1,17 @@
 mochikit.jar:
 % content mochikit %content/
   content/browser-harness.xul (browser-harness.xul)
   content/browser-test.js (browser-test.js)
   content/browser-test-overlay.xul (browser-test-overlay.xul)
   content/cc-analyzer.js (cc-analyzer.js)
   content/chrome-harness.js (chrome-harness.js)
+  content/mochitest-e10s-utils.js (mochitest-e10s-utils.js)
+  content/mochitest-e10s-utils-content.js (mochitest-e10s-utils-content.js)
   content/harness.xul (harness.xul)
   content/redirect.html (redirect.html)
   content/server.js (server.js)
   content/chunkifyTests.js (chunkifyTests.js)
   content/manifestLibrary.js (manifestLibrary.js)
   content/dynamic/getMyDirectory.sjs (dynamic/getMyDirectory.sjs)
   content/static/harness.css (static/harness.css)
   content/tests/SimpleTest/ChromePowers.js (tests/SimpleTest/ChromePowers.js)
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/mochitest-e10s-utils-content.js
@@ -0,0 +1,16 @@
+// This is the content script for mochitest-e10s-utils
+
+// We hook up some events and forward them back to the parent for the tests
+// This is only a partial solution to tests using these events - tests which
+// check, eg, event.target is the content window are still likely to be
+// confused.
+// But it's a good start...
+["load", "DOMContentLoaded", "pageshow"].forEach(eventName => {
+  addEventListener(eventName, function eventHandler(event) {
+    // Some tests also rely on load events from, eg, iframes, so we should see
+    // if we can do something sane to support that too.
+    if (event.target == content.document) {
+      sendAsyncMessage("Test:Event", {name: event.type});
+    }
+  }, true);
+});
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/mochitest-e10s-utils.js
@@ -0,0 +1,87 @@
+// Utilities for running tests in an e10s environment.
+
+// There are some tricks/shortcuts that test code takes that we don't see
+// in the real browser code.  These include setting content.location.href
+// (which doesn't work in test code with e10s enabled as the document object
+// is yet to be created), waiting for certain events the main browser doesn't
+// care about and so doesn't normally get special support, eg, the "pageshow"
+// or "load" events).
+// So we make some hacks to pretend these work in the test suite.
+
+// Ideally all these hacks could be removed, but this can only happen when
+// the tests are refactored to not use these tricks.  But this would be a huge
+// amount of work and is unlikely to happen anytime soon...
+
+const CONTENT_URL = "chrome://mochikit/content/mochitest-e10s-utils-content.js";
+
+// This is an object that is used as the "location" on a remote document or
+// window.  It will be overwritten as the real document and window are made
+// available.
+let locationStub = function(browser) {
+  this.browser = browser;
+};
+locationStub.prototype = {
+  get href() {
+    return this.browser.webNavigation.currentURI.spec;
+  },
+  set href(val) {
+    this.browser.loadURI(val);
+  },
+  assign: function(url) {
+    this.href = url;
+  }
+};
+
+// This object is used in place of contentWindow while we wait for it to be
+// overwritten as the real window becomes available.
+let TemporaryWindowStub = function(browser) {
+  this._locationStub = new locationStub(browser);
+};
+
+TemporaryWindowStub.prototype = {
+  // save poor developers from getting confused about why the window isn't
+  // working like a window should..
+  toString: function() {
+    return "[Window Stub for e10s tests]";
+  },
+  get location() {
+    return this._locationStub;
+  },
+  set location(val) {
+    this._locationStub.href = val;
+  },
+  get document() {
+    // so tests can say: document.location....
+    return this;
+  }
+};
+
+// An observer called when a new remote browser element is created.  We replace
+// the _contentWindow in new browsers with our TemporaryWindowStub object.
+function observeNewFrameloader(subject, topic, data) {
+  let browser = subject.QueryInterface(Ci.nsIFrameLoader).ownerElement;
+  browser._contentWindow = new TemporaryWindowStub(browser);
+}
+
+function e10s_init() {
+  // Use the global message manager to inject a content script into all browsers.
+  let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
+                   .getService(Ci.nsIMessageListenerManager);
+  globalMM.loadFrameScript(CONTENT_URL, true);
+  globalMM.addMessageListener("Test:Event", function(message) {
+    let event = document.createEvent('HTMLEvents');
+    event.initEvent(message.data.name, true, true, {});
+    message.target.dispatchEvent(event);
+  });
+
+  // We add an observer so we can notice new <browser> elements created
+  Services.obs.addObserver(observeNewFrameloader, "remote-browser-shown", false);
+
+  // Listen for an 'oop-browser-crashed' event and log it so people analysing
+  // test logs have a clue about what is going on.
+  window.addEventListener("oop-browser-crashed", (event) => {
+    let uri = event.target.currentURI;
+    Cu.reportError("remote browser crashed while on " +
+                   (uri ? uri.spec : "<unknown>") + "\n");
+  }, true);
+}