merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 05 Oct 2015 11:57:59 +0200
changeset 265954 b56aeea0c4701677ffda6417183caa60d2a6a4a7
parent 265946 0622c833f6dac163da64cbcc7cabfd2ac34018d0 (current diff)
parent 265953 50a192e5482ba82a724d6630df5089ecbd99125c (diff)
child 265984 45f01961ecd0ad9a45067f3e08bfb92539042eeb
push id29475
push usercbook@mozilla.com
push dateMon, 05 Oct 2015 09:58:08 +0000
treeherderautoland@b56aeea0c470 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.0a1
first release with
nightly linux32
b56aeea0c470 / 44.0a1 / 20151005030206 / files
nightly linux64
b56aeea0c470 / 44.0a1 / 20151005030206 / files
nightly mac
b56aeea0c470 / 44.0a1 / 20151005030206 / files
nightly win32
b56aeea0c470 / 44.0a1 / 20151005030206 / files
nightly win64
b56aeea0c470 / 44.0a1 / 20151005030206 / 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 mozilla-central a=merge
--- a/addon-sdk/source/test/jetpack-package.ini
+++ b/addon-sdk/source/test/jetpack-package.ini
@@ -139,17 +139,17 @@ skip-if = true
 [test-test-utils-async.js]
 [test-test-utils-generator.js]
 [test-test-utils-sync.js]
 [test-test-utils.js]
 [test-text-streams.js]
 [test-timer.js]
 [test-traceback.js]
 [test-ui-action-button.js]
-skip-if = (os == "linux" || os == "win") && debug
+skip-if = debug || asan # Bug 1208727
 [test-ui-frame.js]
 [test-ui-id.js]
 [test-ui-sidebar-private-browsing.js]
 [test-ui-sidebar.js]
 [test-ui-toggle-button.js]
 [test-ui-toolbar.js]
 [test-unit-test-finder.js]
 [test-unit-test.js]
--- a/browser/base/content/abouthome/aboutHome.js
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -16,17 +16,17 @@ const DEFAULT_SNIPPETS_URLS = [
 
 const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
 
 // IndexedDB storage constants.
 const DATABASE_NAME = "abouthome";
 const DATABASE_VERSION = 1;
 const DATABASE_STORAGE = "persistent";
 const SNIPPETS_OBJECTSTORE_NAME = "snippets";
-var searchText, findKey;
+var searchText;
 
 // This global tracks if the page has been set up before, to prevent double inits
 var gInitialized = false;
 var gObserver = new MutationObserver(function (mutations) {
   for (let mutation of mutations) {
     if (mutation.attributeName == "snippetsVersion") {
       if (!gInitialized) {
         ensureSnippetsMapThen(loadSnippets);
@@ -50,24 +50,30 @@ window.addEventListener("pageshow", func
   document.dispatchEvent(event);
 });
 
 window.addEventListener("pagehide", function() {
   window.gObserver.disconnect();
   window.removeEventListener("resize", fitToWidth);
 });
 
-// make Accel+f focus the search box
 window.addEventListener("keypress", ev => {
-  // Make Ctrl/Cmd+f focus the search box.
-  let modifiers = ev.ctrlKey + ev.altKey + ev.shiftKey + ev.metaKey;
-  if (ev.getModifierState("Accel") && modifiers == 1 && ev.key == findKey) {
-    searchText.focus();
-    ev.preventDefault();
-  }
+  // focus the search-box on keypress
+  if (document.activeElement.id == "searchText") // unless already focussed
+    return;
+
+  let modifiers = ev.ctrlKey + ev.altKey + ev.metaKey;
+  // ignore Ctrl/Cmd/Alt, but not Shift
+  // also ignore Tab, Insert, PageUp, etc., and Space
+  if (modifiers != 0 || ev.charCode == 0 || ev.charCode == 32)
+    return;
+
+  searchText.focus();
+  // need to send the first keypress outside the search-box manually to it
+  searchText.value += ev.key;
 });
 
 // This object has the same interface as Map and is used to store and retrieve
 // the snippets data.  It is lazily initialized by ensureSnippetsMapThen(), so
 // be sure its callback returned before trying to use it.
 var gSnippetsMap;
 var gSnippetsMapCallbacks = [];
 
@@ -190,17 +196,16 @@ function setupSearch()
   // The "autofocus" attribute doesn't focus the form element
   // immediately when the element is first drawn, so the
   // attribute is also used for styling when the page first loads.
   searchText = document.getElementById("searchText");
   searchText.addEventListener("blur", function searchText_onBlur() {
     searchText.removeEventListener("blur", searchText_onBlur);
     searchText.removeAttribute("autofocus");
   });
-  findKey = searchText.dataset.findkey;
 
   if (!gContentSearchController) {
     gContentSearchController =
       new ContentSearchUIController(searchText, searchText.parentNode,
                                     "abouthome", "homepage");
   }
 }
 
--- a/browser/base/content/abouthome/aboutHome.xhtml
+++ b/browser/base/content/abouthome/aboutHome.xhtml
@@ -38,17 +38,17 @@
     <div class="spacer"/>
     <div id="topSection">
       <div id="brandLogo"></div>
 
       <div id="searchIconAndTextContainer">
         <div id="searchIcon"/>
         <input type="text" name="q" value="" id="searchText" maxlength="256"
                aria-label="&contentSearchInput.label;" autofocus="autofocus"
-               dir="auto" data-findkey="&find.commandkey;"/>
+               dir="auto"/>
         <input id="searchSubmit" type="button" value="" onclick="onSearchSubmit(event)"
                aria-label="&contentSearchSubmit.label;"/>
       </div>
 
       <div id="snippetContainer">
         <div id="defaultSnippets" hidden="true">
           <span id="defaultSnippet1">&abouthome.defaultSnippet1.v1;</span>
           <span id="defaultSnippet2">&abouthome.defaultSnippet2.v1;</span>
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -415,29 +415,30 @@ var gTests = [
       searchController.selectedIndex = 1;
       EventUtils.synthesizeMouseAtCenter(row, {button: 0}, gBrowser.contentWindow);
       yield loadPromise;
       ok(input.value == "x", "Input value did not change");
     });
   }
 },
 {
-  desc: "Cmd+f should focus the search box in the page",
+  desc: "Pressing any key should focus the search box in the page, and send the key to it",
   setup: function () {},
   run: Task.async(function* () {
     let doc = gBrowser.selectedBrowser.contentDocument;
     let logo = doc.getElementById("brandLogo");
     let searchInput = doc.getElementById("searchText");
 
     EventUtils.synthesizeMouseAtCenter(logo, {});
     isnot(searchInput, doc.activeElement, "Search input should not be the active element.");
 
-    EventUtils.synthesizeKey("f", { accelKey: true });
+    EventUtils.synthesizeKey("a", {});
     yield promiseWaitForCondition(() => doc.activeElement === searchInput);
     is(searchInput, doc.activeElement, "Search input should be the active element.");
+    is(searchInput.value, "a", "Search input should be 'a'.");
   })
 },
 {
   desc: "Cmd+k should focus the search box in the page when the search box in the toolbar is absent",
   setup: function () {
     // Remove the search bar from toolbar
     CustomizableUI.removeWidgetFromArea("search-container");
   },
@@ -486,16 +487,35 @@ var gTests = [
     yield EventUtils.synthesizeMouseAtCenter(syncButton, {}, gBrowser.contentWindow);
 
     let result = yield openPrefsPromise;
     window.openPreferences = oldOpenPrefs;
 
     is(result.pane, "paneSync", "openPreferences should be called with paneSync");
     is(result.params.urlParams.entrypoint, "abouthome", "openPreferences should be called with abouthome entrypoint");
   })
+},
+{
+  desc: "Pressing Space while the Addons button is focussed should activate it",
+  setup: function () {},
+  run: Task.async(function* () {
+    // Skip this test on Mac, because Space doesn't activate the button there.
+    if (navigator.platform.indexOf("Mac") == 0) {
+      return Promise.resolve();
+    }
+
+    info("Waiting for about:addons tab to open...");
+    let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+    let addOnsButton = gBrowser.selectedBrowser.contentDocument.getElementById("addons");
+    addOnsButton.focus();
+    EventUtils.synthesizeKey(" ", {});
+    let tab = yield promiseTabOpened;
+    is(tab.linkedBrowser.currentURI.spec, "about:addons", "Should have seen the about:addons tab");
+    yield BrowserTestUtils.removeTab(tab);
+  })
 }
 
 ];
 
 function test()
 {
   waitForExplicitFinish();
   requestLongerTimeout(2);
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -752,19 +752,17 @@ body[platform="win"] .share-service-drop
   background-image: url("../img/icons-16x16.svg#add-hover");
 }
 
 .dropdown-menu-item:hover:active > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-active");
 }
 
 .context-url-view-wrapper {
-  /* 18px for indent of .text-chat-arrow, 1px for border of .text-chat-entry > p,
-     0.5rem for padding of .text-chat-entry > p */
-  padding: calc(18px - 1px - 0.5rem);
+  padding: 12px;
   margin-bottom: 0.5em;
   background-color: #dbf7ff;
 }
 
 .showing-room-name > .text-chat-entries > .text-chat-scroller > .context-url-view-wrapper {
   padding-top: 0;
 }
 
@@ -1177,163 +1175,145 @@ body[platform="win"] .share-service-drop
 }
 
 /* Text chat in styles */
 
 .text-chat-view {
   background: white;
 }
 
-.text-chat-box {
-  flex: 0 0 auto;
-  max-height: 40px;
-  min-height: 40px;
-  width: 100%;
-}
-
 .text-chat-entries {
   overflow: auto;
 }
 
-.text-chat-entry {
+.text-chat-entry,
+.text-chat-header {
   display: flex;
-  flex-direction: row;
-  margin-right: .2em;
   margin-bottom: .5em;
-  text-align: start;
-  flex-wrap: nowrap;
-  justify-content: flex-start;
-  align-content: stretch;
   align-items: flex-start;
 }
 
-html[dir="rtl"] .text-chat-entry {
-  margin-right: auto;
-  margin-left: .2em;
+.text-chat-entry {
+  /* aligns paragraph to side where reading starts from */
+  text-align: start;
+}
+
+/* Sent text chat entries should be on the right */
+.text-chat-entry.sent {
+  /* aligns paragraph to right side */
+  justify-content: flex-end;
+  margin-left: 0;
+  margin-right: 5px;
+}
+
+.text-chat-entry.received {
+  margin-left: 4px;
+  margin-right: 0;
+}
+
+html[dir="rtl"] .text-chat-entry.sent {
+  margin-left: 5px;
+  margin-right: 0;
+}
+
+
+html[dir="rtl"] .text-chat-entry.received {
+  margin-left: 0;
+  margin-right: 5px;
 }
 
 /* If you change this entry, check it doesn't affect the "special" text
    chat entries as well (.speical, .room-name, .context-url-view-wrapper */
 .text-chat-entry > p {
   position: relative;
   z-index: 10;
   /* Drop the default margins from the 'p' element. */
   margin: 0;
-  padding: .5rem;
+  padding: .8rem;
   /* leave some room for the chat bubble arrow */
-  max-width: 80%;
-  border-width: 1px;
-  border-style: solid;
-  border-color: #5cccee;
+  max-width: 70%;
+  border-radius: 15px;
+  border: 1px solid #5cccee;
   background: #fff;
   word-wrap: break-word;
   flex: 0 1 auto;
-  align-self: auto;
-}
-
-.text-chat-entry.sent > p,
-.text-chat-entry.received > p {
-  background: #fff;
+  order: 1;
 }
 
 .text-chat-entry.sent > p {
-  border-radius: 15px;
   border-bottom-right-radius: 0;
 }
 
 .text-chat-entry.received > p {
-  border-radius: 15px;
   border-top-left-radius: 0;
+  border-color: #d8d8d8;
 }
 
 html[dir="rtl"] .text-chat-entry.sent > p {
-  border-radius: 15px;
   border-bottom-left-radius: 0;
+  border-bottom-right-radius: 15px;
 }
 
 html[dir="rtl"] .text-chat-entry.received > p {
-  border-radius: 15px;
   border-top-right-radius: 0;
-}
-
-.text-chat-entry.received > p {
-  order: 1;
-}
-
-.text-chat-entry.sent > p {
-  order: 1;
-}
-
-.text-chat-entry.received {
-  text-align: start;
-}
-
-.text-chat-entry.received > p {
-  border-color: #d8d8d8;
+  border-top-left-radius: 15px;
 }
 
 /* Text chat entry timestamp */
 .text-chat-entry-timestamp {
   margin: 0 .5em;
   color: #aaa;
   font-style: italic;
   font-size: .8em;
   flex: 0 1 auto;
   align-self: center;
 }
 
-/* Sent text chat entries should be on the right */
-.text-chat-entry.sent {
-  justify-content: flex-end;
-}
-
 .received > .text-chat-entry-timestamp {
   order: 2;
 }
 
-/* Pseudo element used to cover part between chat bubble and chat arrow. */
+/* Pseudo element used to cover part between chat bubble and chat arrow.
+   dimensions may change for each position */
 .text-chat-entry > p:after {
   position: absolute;
   background: #fff;
   content: "";
+  /* default dimensions */
+  width: 6px;
+  height: 7px;
 }
 
 .text-chat-entry.sent > p:after {
-  right: -2px;
+  right: -1px;
   bottom: 0;
-  width: 15px;
-  height: 9px;
-  border-top-left-radius: 15px;
-  border-top-right-radius: 22px;
+  width: 7px;
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
 }
 
 .text-chat-entry.received > p:after {
   top: 0;
-  left: -2px;
-  width: 15px;
-  height: 9px;
-  border-bottom-left-radius: 22px;
-  border-bottom-right-radius: 15px;
+  left: -1px;
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
 }
 
 html[dir="rtl"] .text-chat-entry.sent > p:after {
   /* Reset */
   right: auto;
   left: -1px;
   bottom: 0;
-  width: 15px;
-  height: 9px;
 }
 
 html[dir="rtl"] .text-chat-entry.received > p:after {
   /* Reset */
-  left: auto;
   top: 0;
   right: -1px;
-  width: 15px;
+  width: 9px;
   height: 6px;
 }
 
 /* Text chat entry arrow */
 .text-chat-arrow {
   width: 18px;
   background-repeat: no-repeat;
   flex: 0 1 auto;
@@ -1369,39 +1349,43 @@ html[dir="rtl"] .text-chat-entry.sent .t
 }
 
 html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
   /* Reset margin. */
   margin-right: 0;
   margin-left: -10px;
 }
 
-.text-chat-entry.special.room-name {
+.text-chat-header.special.room-name {
   color: black;
   font-weight: bold;
   text-align: start;
   background-color: #dbf7ff;
   margin-bottom: 0;
   margin-right: 0;
 }
 
-.text-chat-entry.special.room-name p {
+.text-chat-header.special.room-name p {
   background: #dbf7ff;
   max-width: 100%;
   /* 18px for indent of .text-chat-arrow, 1px for border of .text-chat-entry > p,
    0.5rem for padding of .text-chat-entry > p */
   padding: calc(18px - 1px - 0.5rem);
 }
 
-.text-chat-entry.special > p {
+.text-chat-header.special > p {
   border: none;
 }
 
 .text-chat-box {
   margin: auto;
+  flex: 0 0 auto;
+  max-height: 40px;
+  min-height: 40px;
+  width: 100%;
 }
 
 .text-chat-box > form > input {
   width: 100%;
   height: 40px;
   padding: 0 .4rem .4rem;
   font-size: 1.1em;
   border: 0;
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -76,17 +76,17 @@ loop.shared.views.chat = (function(mozL1
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
       message: React.PropTypes.string.isRequired
     },
 
     render: function() {
       return (
-        React.createElement("div", {className: "text-chat-entry special room-name"}, 
+        React.createElement("div", {className: "text-chat-header special room-name"}, 
           React.createElement("p", null, mozL10n.get("rooms_welcome_title", {conversationName: this.props.message}))
         )
       );
     }
   });
 
   /**
    * Manages the text entries in the chat entries view. This is split out from
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -76,17 +76,17 @@ loop.shared.views.chat = (function(mozL1
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
       message: React.PropTypes.string.isRequired
     },
 
     render: function() {
       return (
-        <div className="text-chat-entry special room-name">
+        <div className="text-chat-header special room-name">
           <p>{mozL10n.get("rooms_welcome_title", {conversationName: this.props.message})}</p>
         </div>
       );
     }
   });
 
   /**
    * Manages the text entries in the chat entries view. This is split out from
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -575,17 +575,17 @@ describe("loop.shared.views.TextChatView
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "A wonderful surprise!",
         roomUrl: "Fake"
       }));
 
       var node = view.getDOMNode();
       expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
 
-      var entries = node.querySelectorAll(".text-chat-entry");
+      var entries = node.querySelectorAll(".text-chat-header");
       expect(entries.length).eql(1);
       expect(entries[0].classList.contains("special")).eql(true);
       expect(entries[0].classList.contains("room-name")).eql(true);
     });
 
     it("should render a special entry for the context url", function() {
       view = mountTestComponent();
 
--- a/browser/locales/en-US/chrome/browser/aboutHome.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutHome.dtd
@@ -32,12 +32,8 @@
 <!ENTITY abouthome.preferencesButtonUnix.label  "Preferences">
 <!ENTITY abouthome.addonsButton.label    "Add-ons">
 <!-- LOCALIZATION NOTE (abouthome.appsButton2.label): This string should be consistent with
      the Apps menu item in the Tools menu (webapps.label in browser.dtd) and the Apps toolbar button in
      Firefox's customization palette (web-apps-button.label in customizableWidgets.properties) -->
 <!ENTITY abouthome.appsButton2.label     "Apps">
 <!ENTITY abouthome.downloadsButton.label "Downloads">
 <!ENTITY abouthome.syncButton.label      "&syncBrand.shortName.label;">
-
-<!-- LOCALIZATION NOTE (find.commandkey): This is the key to use in
-     conjunction with accel (Command on Mac or Ctrl on other platforms) to find -->
-<!ENTITY find.commandkey "f">
--- a/mobile/android/chrome/content/aboutLogins.js
+++ b/mobile/android/chrome/content/aboutLogins.js
@@ -1,16 +1,17 @@
 /* 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 Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
+Cu.import("resource://services-common/utils.js"); /*global: CommonUtils */
 Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 
 XPCOMUtils.defineLazyGetter(window, "gChromeWin", () =>
   window.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIWebNavigation)
     .QueryInterface(Ci.nsIDocShellTreeItem)
     .rootTreeItem
@@ -44,74 +45,129 @@ const LOGIN_VIEWED = 1;
 const LOGIN_EDITED = 2;
 const LOGIN_PW_TOGGLED = 3;
 
 var Logins = {
   _logins: [],
   _filterTimer: null,
   _selectedLogin: null,
 
-  _getLogins: function() {
-    let logins;
+  // Load the logins list, displaying interstitial UI (see
+  // #logins-list-loading-body) while loading.  There are careful
+  // jank-avoiding measures taken in this function; be careful when
+  // modifying it!
+  //
+  // Returns a Promise that resolves to the list of logins, ordered by
+  // hostname.
+  _promiseLogins: function() {
     let contentBody = document.getElementById("content-body");
     let emptyBody = document.getElementById("empty-body");
     let filterIcon = document.getElementById("filter-button");
 
-    this._toggleListBody(true);
-    emptyBody.classList.add("hidden");
+    let showSpinner = () => {
+      this._toggleListBody(true);
+      emptyBody.classList.add("hidden");
+    };
+
+    let getAllLogins = () => {
+      let logins = [];
+      try {
+        TelemetryStopwatch.start("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS");
+        logins = Services.logins.getAllLogins();
+        TelemetryStopwatch.finish("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS");
+      } catch(e) {
+        // It's likely that the Master Password was not entered; give
+        // a hint to the next person.
+        throw new Error("Possible Master Password permissions error: " + e.toString());
+      }
 
-    try {
-      TelemetryStopwatch.start("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS");
-      logins = Services.logins.getAllLogins();
-      TelemetryStopwatch.finish("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS");
-    } catch(e) {
-      // Master password was not entered
-      debug("Master password permissions error: " + e);
-      logins = [];
-    }
-    this._toggleListBody(false);
+      logins.sort((a, b) => a.hostname.localeCompare(b.hostname));
+
+      return logins;
+    };
+
+    let hideSpinner = (logins) => {
+      this._toggleListBody(false);
+
+      if (!logins.length) {
+        contentBody.classList.add("hidden");
+        filterIcon.classList.add("hidden");
+        emptyBody.classList.remove("hidden");
+      } else {
+        contentBody.classList.remove("hidden");
+        emptyBody.classList.add("hidden");
+      }
+
+      return logins;
+    };
 
-    if (!logins.length) {
-      emptyBody.classList.remove("hidden");
+    // Return a promise that is resolved after a paint.
+    let waitForPaint = () => {
+      // We're changing 'display'.  We need to wait for the new value to take
+      // effect; otherwise, we'll block and never paint a change.  Since
+      // requestAnimationFrame callback is generally triggered *before* any
+      // style flush and layout, we wait for two animation frames.  This
+      // approach was cribbed from
+      // https://dxr.mozilla.org/mozilla-central/rev/5abe3c4deab94270440422c850bbeaf512b1f38d/browser/base/content/browser-fullScreen.js?offset=0#469.
+      return new Promise(function(resolve, reject) {
+        requestAnimationFrame(() => {
+          requestAnimationFrame(() => {
+            resolve();
+          });
+        });
+      });
+    };
 
-      filterIcon.classList.add("hidden");
-      contentBody.classList.add("hidden");
-    } else {
-      emptyBody.classList.add("hidden");
+    // getAllLogins janks the main-thread.  We need to paint before that jank;
+    // by throwing the janky load onto the next tick, we paint the spinner; the
+    // spinner is CSS animated off-main-thread.
+    return Promise.resolve()
+      .then(showSpinner)
+      .then(waitForPaint)
+      .then(getAllLogins)
+      .then(hideSpinner);
+  },
 
-      filterIcon.classList.remove("hidden");
-    }
-
-    logins.sort((a, b) => a.hostname.localeCompare(b.hostname));
-    return this._logins = logins;
+  // Reload the logins list, displaying interstitial UI while loading.
+  // Update the stored and displayed list upon completion.
+  _reloadList: function() {
+    this._promiseLogins()
+      .then((logins) => {
+        this._logins = logins;
+        this._loadList(logins);
+      })
+      .catch((e) => {
+        // There's no way to recover from errors, sadly.  Log and make
+        // it obvious that something is up.
+        this._logins = [];
+        debug("Failed to _reloadList!");
+        Cu.reportError(e);
+      });
   },
 
   _toggleListBody: function(isLoading) {
     let contentBody = document.getElementById("content-body");
     let loadingBody = document.getElementById("logins-list-loading-body");
 
     if (isLoading) {
       contentBody.classList.add("hidden");
       loadingBody.classList.remove("hidden");
     } else {
       loadingBody.classList.add("hidden");
       contentBody.classList.remove("hidden");
     }
-
   },
 
   init: function () {
     window.addEventListener("popstate", this , false);
 
     Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
     document.getElementById("update-btn").addEventListener("click", this._onSaveEditLogin.bind(this), false);
     document.getElementById("password-btn").addEventListener("click", this._onPasswordBtn.bind(this), false);
 
-    this._loadList(this._getLogins());
-
     let filterInput = document.getElementById("filter-input");
     let filterContainer = document.getElementById("filter-input-container");
 
     filterInput.addEventListener("input", (event) => {
       // Stop any in-progress filter timer
       if (this._filterTimer) {
         clearTimeout(this._filterTimer);
         this._filterTimer = null;
@@ -142,16 +198,18 @@ var Logins = {
       filterInput.blur();
       filterInput.value = "";
       this._loadList(this._logins);
     }, false);
 
     this._showList();
 
     this._updatePasswordBtn(true);
+
+    this._reloadList();
   },
 
   uninit: function () {
     Services.obs.removeObserver(this, "passwordmgr-storage-changed");
     window.removeEventListener("popstate", this, false);
   },
 
   _loadList: function (logins) {
@@ -434,18 +492,17 @@ var Logins = {
         break;
       }
     }
   },
 
   observe: function (subject, topic, data) {
     switch(topic) {
       case "passwordmgr-storage-changed": {
-        // Reload logins content.
-        this._loadList(this._getLogins());
+        this._reloadList();
         break;
       }
     }
   },
 
   _filter: function(event) {
     let value = event.target.value.toLowerCase();
     let logins = this._logins.filter((login) => {
--- a/mobile/android/tests/browser/chrome/chrome.ini
+++ b/mobile/android/tests/browser/chrome/chrome.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 skip-if = os != 'android'
 support-files =
   basic_article.html
   desktopmode_user_agent.sjs
   devicesearch.xml
+  head.js
   session_formdata_sample.html
   simpleservice.xml
   video_controls.html
   video_discovery.html
   web_channel.html
 
 [test_about_logins.html]
 [test_accounts.html]
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/head.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function promiseBrowserEvent(browser, eventType) {
+  return new Promise((resolve) => {
+    function handle(event) {
+      // Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
+      if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
+        info("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
+        return;
+      }
+      info("Received event " + eventType + " from browser");
+      browser.removeEventListener(eventType, handle, true);
+      resolve(event);
+    }
+
+    browser.addEventListener(eventType, handle, true);
+    info("Now waiting for " + eventType + " event from browser");
+  });
+}
+
+function promiseNotification(topic) {
+  Cu.import("resource://gre/modules/Services.jsm");
+
+  return new Promise((resolve, reject) => {
+    function observe(subject, topic, data) {
+      info("Received " + topic + " notification from Gecko");
+      Services.obs.removeObserver(observe, topic);
+      resolve();
+    }
+    Services.obs.addObserver(observe, topic, false);
+    info("Now waiting for " + topic + " notification from Gecko");
+  });
+}
--- a/mobile/android/tests/browser/chrome/test_about_logins.html
+++ b/mobile/android/tests/browser/chrome/test_about_logins.html
@@ -3,21 +3,23 @@
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=1136477
 Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 1136477</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
-  <script type="application/javascript;version=1.7">
+  <script type="application/javascript;version=1.8" src="head.js"></script>
+  <script type="application/javascript;version=1.8">
 
-  "use strict"
+  "use strict";
 
   const { interfaces: Ci, utils: Cu } = Components;
 
   Cu.import("resource://gre/modules/AppConstants.jsm");
   Cu.import("resource://gre/modules/Services.jsm");
 
   const LOGIN_FIELDS = {
     hostname: "http://example.org/tests/robocop/robocop_blank_01.html",
@@ -26,63 +28,72 @@ Migrated from Robocop: https://bugzilla.
     username: "username1",
     password: "password1",
     usernameField: "",
     passwordField: ""
   };
 
   const LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
 
-  let BrowserApp;
-  let browser;
-
-  SimpleTest.waitForExplicitFinish();
-
   function add_login(login) {
     let newLogin = new LoginInfo(login.hostname,
                                  login.formSubmitUrl,
                                  login.realmAny,
                                  login.username,
                                  login.password,
                                  login.usernameField,
                                  login.passwordField);
 
     Services.logins.addLogin(newLogin);
   }
 
-  function password_setup() {
+  add_task(function* test_passwords_list() {
     add_login(LOGIN_FIELDS);
 
     // Load about:logins.
-    BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
-    browser = BrowserApp.addTab("about:logins", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+    let BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+    let browser = BrowserApp.addTab("about:logins", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+
+    yield promiseBrowserEvent(browser, "load");
+
+    let logins_list_parent = browser.contentDocument.getElementById("logins-list").parentNode;
 
-    browser.addEventListener("load", function handle(event) {
-      browser.removeEventListener("load", handle, true);
-      Services.tm.mainThread.dispatch(test_passwords_list, Ci.nsIThread.DISPATCH_NORMAL);
-    }, true);
-  }
+    let waitForLoginToBeAdded = new Promise((resolve) => {
+      let observer = new MutationObserver((mutations) => {
+        for (let mutation of mutations) {
+          for (let node of mutation.addedNodes) {
+            if (node.id == 'logins-list') {
+              info("Received mutation replacing 'logins-list'");
+              resolve();
+              return;
+            }
+          }
+        }
+        info("Skipping spurious mutation not replacing 'logins-list'");
+      });
+      observer.observe(logins_list_parent, {
+        childList: true,
+      });
+      info("Now waiting for mutation to replace 'logins-list'");
+    });
 
-  function test_passwords_list() {
-    // Test that the (single) entry added in setup is correct.
+    yield waitForLoginToBeAdded;
+
     let logins_list = browser.contentDocument.getElementById("logins-list");
 
+    // Test that the (single) entry added in setup is correct.
     let hostname = logins_list.querySelector(".hostname");
     is(hostname.textContent, LOGIN_FIELDS.hostname, "hostname is correct");
 
     let username = logins_list.querySelector(".username");
     is(username.textContent, LOGIN_FIELDS.username, "username is correct");
 
     // Cleanup: close about:logins, opened in password_setup()
     BrowserApp.closeTab(BrowserApp.selectedTab);
-
-    SimpleTest.finish();
-  }
-
-  password_setup();
+  });
 
   </script>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136477">Mozilla Bug 1136477</a>
 <br>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testAboutLogins</a>
 <p id="display"></p>
--- a/mobile/android/tests/browser/chrome/test_desktop_useragent.html
+++ b/mobile/android/tests/browser/chrome/test_desktop_useragent.html
@@ -6,42 +6,25 @@ Migrated from Robocop: https://bugzilla.
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 1157319</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript;version=1.7">
 
   "use strict";
 
   const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
   Cu.import("resource://gre/modules/Services.jsm");
 
-  function promiseBrowserEvent(browser, eventType) {
-    return new Promise((resolve) => {
-      function handle(event) {
-        // Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
-        if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
-          info("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
-          return;
-        }
-        info("Received event " + eventType + " from browser");
-        browser.removeEventListener(eventType, handle, true);
-        resolve(event);
-      }
-
-      browser.addEventListener(eventType, handle, true);
-      info("Now waiting for " + eventType + " event from browser");
-    });
-  }
-
   // Load a custom sjs script that echos our "User-Agent" header back at us
   const TestURI = Services.io.newURI("http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/desktopmode_user_agent.sjs", null, null);
 
   add_task(function* test_desktopmode() {
     let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
     let BrowserApp = chromeWin.BrowserApp;
 
     // Add a new 'desktop mode' tab with our test page
--- a/mobile/android/tests/browser/chrome/test_offline_page.html
+++ b/mobile/android/tests/browser/chrome/test_offline_page.html
@@ -6,44 +6,27 @@ Migrated from Robocop: https://bugzilla.
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 1089190</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript;version=1.7">
 
   "use strict";
 
   const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
   Cu.import("resource://gre/modules/Services.jsm");
   Cu.import("resource://gre/modules/Messaging.jsm");
   Cu.import("resource://gre/modules/Task.jsm");
 
-  function promiseBrowserEvent(browser, eventType) {
-    return new Promise((resolve) => {
-      function handle(event) {
-        // Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
-        if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
-          info("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
-          return;
-        }
-        info("Received event " + eventType + " from browser");
-        browser.removeEventListener(eventType, handle, true);
-        resolve(event);
-      }
-
-      browser.addEventListener(eventType, handle, true);
-      info("Now waiting for " + eventType + " event from browser");
-    });
-  }
-
   // Provide a helper to yield until we are sure the offline state has changed
   function promiseOffline(isOffline) {
     return new Promise((resolve, reject) => {
       function observe(subject, topic, data) {
         info("Received topic: " + topic);
         Services.obs.removeObserver(observe, "network:offline-status-changed");
         resolve();
       }
--- a/mobile/android/tests/browser/chrome/test_reader_view.html
+++ b/mobile/android/tests/browser/chrome/test_reader_view.html
@@ -6,50 +6,23 @@ Migrated from Robocop: https://bugzilla.
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 1158885</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript;version=1.7">
 
   const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
   Cu.import("resource://gre/modules/Services.jsm");
 
-  function promiseBrowserEvent(browser, eventType) {
-    return new Promise((resolve) => {
-      function handle(event) {
-        // Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
-        if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
-          info("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
-          return;
-        }
-        info("Received event " + eventType + " from browser");
-        browser.removeEventListener(eventType, handle, true);
-        resolve(event);
-      }
-
-      browser.addEventListener(eventType, handle, true);
-      info("Now waiting for " + eventType + " event from browser");
-    });
-  }
-
-  function promiseNotification(topic) {
-    return new Promise((resolve, reject) => {
-      function observe(subject, topic, data) {
-        Services.obs.removeObserver(observe, topic);
-        resolve();
-      }
-      Services.obs.addObserver(observe, topic, false);
-    });
-  }
-
   add_task(function* test_reader_view_visibility() {
     let gWin = Services.wm.getMostRecentWindow("navigator:browser");
     let BrowserApp = gWin.BrowserApp;
 
     let url = "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/basic_article.html";
     let browser = BrowserApp.addTab("about:reader?url=" + url).browser;
 
     SimpleTest.registerCleanupFunction(function() {
--- a/mobile/android/tests/browser/chrome/test_session_form_data.html
+++ b/mobile/android/tests/browser/chrome/test_session_form_data.html
@@ -6,16 +6,17 @@ Migrated from Robocop: https://bugzilla.
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 671993</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript;version=1.7">
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -33,35 +34,16 @@ function sleep(wait) {
       notify: function () {
         dump("sleep end");
         resolve();
       },
     }, wait, gTimer.TYPE_ONE_SHOT);
   });
 }
 
-function promiseBrowserLoaded(browser, eventType="load") {
-  return new Promise((resolve, reject) => {
-    dump("Wait browser event: " + eventType);
-
-    function handle(event) {
-      if (event.target != browser.contentDocument) {
-        dump("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
-        return;
-      }
-
-      browser.removeEventListener(eventType, handle, true);
-      dump("Browser event received: " + eventType);
-      resolve(event);
-    }
-
-    browser.addEventListener(eventType, handle, true);
-  });
-}
-
 function queryElement(contentWindow, data) {
   let frame = contentWindow;
   if (data.hasOwnProperty("frame")) {
     frame = contentWindow.frames[data.frame];
   }
 
   let doc = frame.document;
 
@@ -124,17 +106,17 @@ add_task(function* test_formdata() {
 
   // Creates a tab, loads a page with some form fields,
   // modifies their values and closes the tab.
   function createAndRemoveTab() {
     return Task.spawn(function () {
       // Create a new tab.
       let tab = gBrowserApp.addTab(URL);
       let browser = tab.browser;
-      yield promiseBrowserLoaded(browser);
+      yield promiseBrowserEvent(browser, "load");
 
       // Modify form data.
       setInputValue(browser, {id: "txt", value: OUTER_VALUE});
       setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
 
       // Remove the tab.
       gBrowserApp.closeTab(tab);
       yield sleep(CLOSE_TAB_WAIT);
@@ -179,17 +161,17 @@ add_task(function* test_formdata2() {
 
   // Creates a tab, loads a page with some form fields,
   // modifies their values and closes the tab.
   function createAndRemoveTab() {
     return Task.spawn(function () {
       // Create a new tab.
       let tab = gBrowserApp.addTab(URL);
       let browser = tab.browser;
-      yield promiseBrowserLoaded(browser);
+      yield promiseBrowserEvent(browser, "load");
 
       // Modify form data.
       setInputValue(browser, {id: "txt", value: OUTER_VALUE});
       setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
 
       // Remove the tab.
       gBrowserApp.closeTab(tab);
       yield sleep(CLOSE_TAB_WAIT);
@@ -200,17 +182,17 @@ add_task(function* test_formdata2() {
   let state = ss.getClosedTabs(gChromeWin);
   let [{formdata}] = state;
   is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
   is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
 
   // Restore the closed tab.
   let closedTabData = ss.getClosedTabs(gChromeWin)[0];
   let browser = ss.undoCloseTab(gChromeWin, closedTabData);
-  yield promiseBrowserLoaded(browser);
+  yield promiseBrowserEvent(browser, "load");
 
   // Check the form data.
   is(getInputValue(browser, {id: "txt"}), OUTER_VALUE, "outer value restored correctly");
   is(getInputValue(browser, {id: "txt", frame: 0}), INNER_VALUE, "inner value restored correctly");
 
   // Remove the tab.
   gBrowserApp.closeTab(gBrowserApp.getTabForBrowser(browser));
 });