Bug 1353571 part 3 - Refactor browser-syncui and browser-fxaccounts. r?markh draft
authorEdouard Oger <eoger@fastmail.com>
Tue, 18 Apr 2017 14:15:43 -0400
changeset 572165 8fe0dde19cead8304d306ca78a506f719f435332
parent 572164 f9fe9ef116ec7f42ab950fa2c0b9ca4d23d46d1e
child 626962 3394c6a5eef5864248bf5af4203c6b6452b19b75
push id57011
push userbmo:eoger@fastmail.com
push dateWed, 03 May 2017 21:00:43 +0000
reviewersmarkh
bugs1353571
milestone55.0a1
Bug 1353571 part 3 - Refactor browser-syncui and browser-fxaccounts. r?markh MozReview-Commit-ID: K790Ag8WZgv
browser/base/content/browser-context.inc
browser/base/content/browser-fxaccounts.js
browser/base/content/browser-menubar.inc
browser/base/content/browser-sync.js
browser/base/content/browser-syncui.js
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/global-scripts.inc
browser/base/content/moz.build
browser/base/content/nsContextMenu.js
browser/base/content/test/general/accounts_testRemoteCommands.html
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_aboutAccounts.js
browser/base/content/test/general/browser_contextmenu.js
browser/base/content/test/general/browser_fxa_web_channel.html
browser/base/content/test/general/browser_fxa_web_channel.js
browser/base/content/test/general/browser_fxaccounts.js
browser/base/content/test/general/browser_syncui.js
browser/base/content/test/general/browser_visibleTabs_contextMenu.js
browser/base/content/test/general/content_aboutAccounts.js
browser/base/content/test/general/fxa_profile_handler.sjs
browser/base/content/test/general/head.js
browser/base/content/test/sync/.eslintrc.js
browser/base/content/test/sync/accounts_testRemoteCommands.html
browser/base/content/test/sync/browser.ini
browser/base/content/test/sync/browser_aboutAccounts.js
browser/base/content/test/sync/browser_fxa_web_channel.html
browser/base/content/test/sync/browser_fxa_web_channel.js
browser/base/content/test/sync/browser_sync.js
browser/base/content/test/sync/content_aboutAccounts.js
browser/base/content/web-panels.xul
browser/base/content/webext-panels.xul
browser/base/jar.mn
browser/base/moz.build
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_987185_syncButton.js
browser/components/customizableui/test/browser_remote_tabs_button.js
browser/components/customizableui/test/browser_synced_tabs_menu.js
browser/components/syncedtabs/SyncedTabsDeckComponent.js
browser/components/syncedtabs/TabListView.js
browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
browser/components/uitour/test/browser_fxa.js
browser/themes/shared/customizableui/panelUI.inc.css
services/fxaccounts/FxAccountsWebChannel.jsm
services/sync/modules/SyncedTabs.jsm
services/sync/modules/UIState.jsm
services/sync/moz.build
services/sync/tests/unit/test_uistate.js
services/sync/tests/unit/xpcshell.ini
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -280,17 +280,17 @@
                 accesskey="&savePageCmd.accesskey2;"
                 oncommand="gContextMenu.savePageAs();"/>
       <menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
       <menu id="context-sendpagetodevice"
                 label="&sendPageToDevice.label;"
                 accesskey="&sendPageToDevice.accesskey;"
                 hidden="true">
         <menupopup id="context-sendpagetodevice-popup"
-                   onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
+                   onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gSync.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
                 oncommand="gContextMenu.viewBGImage(event);"
                 onclick="checkForMiddleClick(this, event);"/>
       <menuitem id="context-undo"
@@ -327,17 +327,17 @@
       <menuitem id="context-searchselect"
                 oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
       <menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
       <menu id="context-sendlinktodevice"
                 label="&sendLinkToDevice.label;"
                 accesskey="&sendLinkToDevice.accesskey;"
                 hidden="true">
         <menupopup id="context-sendlinktodevice-popup"
-                   onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
+                   onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
       </menu>
       <menuitem id="context-shareselect"
                 label="&shareSelect.label;"
                 accesskey="&shareSelect.accesskey;"
                 oncommand="gContextMenu.shareSelect();"/>
       <menuseparator id="frame-sep"/>
       <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
         <menupopup>
deleted file mode 100644
--- a/browser/base/content/browser-fxaccounts.js
+++ /dev/null
@@ -1,398 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var gFxAccounts = {
-
-  _initialized: false,
-  _cachedProfile: null,
-
-  get weave() {
-    delete this.weave;
-    return this.weave = Cc["@mozilla.org/weave/service;1"]
-                          .getService(Ci.nsISupports)
-                          .wrappedJSObject;
-  },
-
-  get topics() {
-    // Do all this dance to lazy-load FxAccountsCommon.
-    delete this.topics;
-    return this.topics = [
-      "weave:service:ready",
-      "weave:service:login:change",
-      "weave:service:setup-complete",
-      "weave:service:sync:error",
-      "weave:ui:login:error",
-      this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
-      this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
-      this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
-    ];
-  },
-
-  get panelUIFooter() {
-    delete this.panelUIFooter;
-    return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
-  },
-
-  get panelUIStatus() {
-    delete this.panelUIStatus;
-    return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
-  },
-
-  get panelUIAvatar() {
-    delete this.panelUIAvatar;
-    return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
-  },
-
-  get panelUILabel() {
-    delete this.panelUILabel;
-    return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
-  },
-
-  get panelUIIcon() {
-    delete this.panelUIIcon;
-    return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
-  },
-
-  get strings() {
-    delete this.strings;
-    return this.strings = Services.strings.createBundle(
-      "chrome://browser/locale/accounts.properties"
-    );
-  },
-
-  get loginFailed() {
-    // Referencing Weave.Service will implicitly initialize sync, and we don't
-    // want to force that - so first check if it is ready.
-    if (!this.weaveService.ready) {
-      return false;
-    }
-    // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
-    // All other login failures are assumed to be transient and should go
-    // away by themselves, so aren't reflected here.
-    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
-  },
-
-  get sendTabToDeviceEnabled() {
-    return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
-  },
-
-  isSendableURI(aURISpec) {
-    if (!aURISpec) {
-      return false;
-    }
-    // Disallow sending tabs with more than 65535 characters.
-    if (aURISpec.length > 65535) {
-      return false;
-    }
-    try {
-      // Filter out un-sendable URIs -- things like local files, object urls, etc.
-      const unsendableRegexp = new RegExp(
-        Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
-      return !unsendableRegexp.test(aURISpec);
-    } catch (e) {
-      // The preference has been removed, or is an invalid regexp, so we log an
-      // error and treat it as a valid URI -- and the more problematic case is
-      // the length, which we've already addressed.
-      Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
-      return true;
-    }
-  },
-
-  get remoteClients() {
-    return Weave.Service.clientsEngine.remoteClients
-           .sort((a, b) => a.name.localeCompare(b.name));
-  },
-
-  init() {
-    // Bail out if we're already initialized and for pop-up windows.
-    if (this._initialized || !window.toolbar.visible) {
-      return;
-    }
-
-    for (let topic of this.topics) {
-      Services.obs.addObserver(this, topic);
-    }
-
-    EnsureFxAccountsWebChannel();
-    this._initialized = true;
-
-    this.updateUI();
-  },
-
-  uninit() {
-    if (!this._initialized) {
-      return;
-    }
-
-    for (let topic of this.topics) {
-      Services.obs.removeObserver(this, topic);
-    }
-
-    this._initialized = false;
-  },
-
-  observe(subject, topic, data) {
-    switch (topic) {
-      case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION:
-        this._cachedProfile = null;
-        // Fallthrough intended
-      default:
-        this.updateUI();
-        break;
-    }
-  },
-
-  // Note that updateUI() returns a Promise that's only used by tests.
-  updateUI() {
-    this.panelUIFooter.hidden = false;
-
-    let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
-    let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
-    let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
-    // The localization string is for the signed in text, but it's the default text as well
-    let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
-
-    let updateWithUserData = (userData) => {
-      // Window might have been closed while fetching data.
-      if (window.closed) {
-        return;
-      }
-
-      // Reset the button to its original state.
-      this.panelUILabel.setAttribute("label", defaultLabel);
-      this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
-      this.panelUIFooter.removeAttribute("fxastatus");
-      this.panelUIAvatar.style.removeProperty("list-style-image");
-      let showErrorBadge = false;
-      if (userData) {
-        // At this point we consider the user as logged-in (but still can be in an error state)
-        if (this.loginFailed) {
-          let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1);
-          this.panelUIFooter.setAttribute("fxastatus", "error");
-          this.panelUILabel.setAttribute("label", errorLabel);
-          this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
-          showErrorBadge = true;
-        } else if (!userData.verified) {
-          let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1);
-          this.panelUIFooter.setAttribute("fxastatus", "error");
-          this.panelUIFooter.setAttribute("unverified", "true");
-          this.panelUILabel.setAttribute("label", unverifiedLabel);
-          this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
-          showErrorBadge = true;
-        } else {
-          this.panelUIFooter.setAttribute("fxastatus", "signedin");
-          this.panelUILabel.setAttribute("label", userData.email);
-        }
-      }
-      if (showErrorBadge) {
-        PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-      } else {
-        PanelUI.removeNotification("fxa-needs-authentication");
-      }
-    }
-
-    let updateWithProfile = (profile) => {
-      if (profile.displayName) {
-        this.panelUILabel.setAttribute("label", profile.displayName);
-      }
-      if (profile.avatar) {
-        let bgImage = "url(\"" + profile.avatar + "\")";
-        this.panelUIAvatar.style.listStyleImage = bgImage;
-
-        let img = new Image();
-        img.onerror = () => {
-          // Clear the image if it has trouble loading. Since this callback is asynchronous
-          // we check to make sure the image is still the same before we clear it.
-          if (this.panelUIAvatar.style.listStyleImage === bgImage) {
-            this.panelUIAvatar.style.removeProperty("list-style-image");
-          }
-        };
-        img.src = profile.avatar;
-      }
-    }
-
-    return fxAccounts.getSignedInUser().then(userData => {
-      // userData may be null here when the user is not signed-in, but that's expected
-      updateWithUserData(userData);
-      // unverified users cause us to spew log errors fetching an OAuth token
-      // to fetch the profile, so don't even try in that case.
-      if (!userData || !userData.verified) {
-        return null; // don't even try to grab the profile.
-      }
-      if (this._cachedProfile) {
-        return this._cachedProfile;
-      }
-      return fxAccounts.getSignedInUserProfile().catch(err => {
-        // Not fetching the profile is sad but the FxA logs will already have noise.
-        return null;
-      });
-    }).then(profile => {
-      if (!profile) {
-        return;
-      }
-      updateWithProfile(profile);
-      this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update
-    }).catch(error => {
-      // This is most likely in tests, were we quickly log users in and out.
-      // The most likely scenario is a user logged out, so reflect that.
-      // Bug 995134 calls for better errors so we could retry if we were
-      // sure this was the failure reason.
-      this.FxAccountsCommon.log.error("Error updating FxA account info", error);
-      updateWithUserData(null);
-    });
-  },
-
-  onMenuPanelCommand() {
-
-    switch (this.panelUIFooter.getAttribute("fxastatus")) {
-    case "signedin":
-      this.openPreferences();
-      break;
-    case "error":
-      if (this.panelUIFooter.getAttribute("unverified")) {
-        this.openPreferences();
-      } else {
-        this.openSignInAgainPage("menupanel");
-      }
-      break;
-    default:
-      this.openPreferences();
-      break;
-    }
-
-    PanelUI.hide();
-  },
-
-  openPreferences() {
-    openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } });
-  },
-
-  openAccountsPage(action, urlParams = {}) {
-    let params = new URLSearchParams();
-    if (action) {
-      params.set("action", action);
-    }
-    for (let name in urlParams) {
-      if (urlParams[name] !== undefined) {
-        params.set(name, urlParams[name]);
-      }
-    }
-    let url = "about:accounts?" + params;
-    switchToTabHavingURI(url, true, {
-      replaceQueryString: true
-    });
-  },
-
-  openSignInAgainPage(entryPoint) {
-    this.openAccountsPage("reauth", { entrypoint: entryPoint });
-  },
-
-  async openDevicesManagementPage(entryPoint) {
-    let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
-    switchToTabHavingURI(url, true, {
-      replaceQueryString: true
-    });
-  },
-
-  sendTabToDevice(url, clientId, title) {
-    Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
-  },
-
-  populateSendTabToDevicesMenu(devicesPopup, url, title) {
-    // remove existing menu items
-    while (devicesPopup.hasChildNodes()) {
-      devicesPopup.firstChild.remove();
-    }
-
-    const fragment = document.createDocumentFragment();
-
-    const onTargetDeviceCommand = (event) => {
-      let clients = event.target.getAttribute("clientId") ?
-        [event.target.getAttribute("clientId")] :
-        this.remoteClients.map(client => client.id);
-
-      clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
-    }
-
-    function addTargetDevice(clientId, name) {
-      const targetDevice = document.createElement("menuitem");
-      targetDevice.addEventListener("command", onTargetDeviceCommand, true);
-      targetDevice.setAttribute("class", "sendtab-target");
-      targetDevice.setAttribute("clientId", clientId);
-      targetDevice.setAttribute("label", name);
-      fragment.appendChild(targetDevice);
-    }
-
-    const clients = this.remoteClients;
-    for (let client of clients) {
-      addTargetDevice(client.id, client.name);
-    }
-
-    // "All devices" menu item
-    if (clients.length > 1) {
-      const separator = document.createElement("menuseparator");
-      fragment.appendChild(separator);
-      const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
-      addTargetDevice("", allDevicesLabel);
-    }
-
-    devicesPopup.appendChild(fragment);
-  },
-
-  updateTabContextMenu(aPopupMenu, aTargetTab) {
-    if (!this.sendTabToDeviceEnabled ||
-        !this.weaveService.ready) {
-      return;
-    }
-
-    const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
-    const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
-
-    ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
-    .forEach(id => { document.getElementById(id).hidden = !showSendTab });
-  },
-
-  initPageContextMenu(contextMenu) {
-    if (!this.sendTabToDeviceEnabled ||
-        !this.weaveService.ready) {
-      return;
-    }
-
-    const remoteClientPresent = this.remoteClients.length > 0;
-    // showSendLink and showSendPage are mutually exclusive
-    let showSendLink = remoteClientPresent
-                       && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
-    const showSendPage = !showSendLink && remoteClientPresent
-                         && !(contextMenu.isContentSelected ||
-                              contextMenu.onImage || contextMenu.onCanvas ||
-                              contextMenu.onVideo || contextMenu.onAudio ||
-                              contextMenu.onLink || contextMenu.onTextInput)
-                         && this.isSendableURI(contextMenu.browser.currentURI.spec);
-
-    if (showSendLink) {
-      // This isn't part of the condition above since we don't want to try and
-      // send the page if a link is clicked on or selected but is not sendable.
-      showSendLink = this.isSendableURI(contextMenu.linkURL);
-    }
-
-    ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
-    .forEach(id => contextMenu.showItem(id, showSendPage));
-    ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
-    .forEach(id => contextMenu.showItem(id, showSendLink));
-  }
-};
-
-XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function() {
-  return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
-});
-
-XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
-  "resource://gre/modules/FxAccountsWebChannel.jsm");
-
-
-XPCOMUtils.defineLazyGetter(gFxAccounts, "weaveService", function() {
-  return Components.classes["@mozilla.org/weave/service;1"]
-                   .getService(Components.interfaces.nsISupports)
-                   .wrappedJSObject;
-});
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -470,49 +470,44 @@
       <!-- Bookmarks menu items -->
     </menupopup>
   </menu>
 
             <menu id="tools-menu"
                   label="&toolsMenu.label;"
                   accesskey="&toolsMenu.accesskey;"
                   onpopupshowing="mirrorShow(this)">
-              <menupopup id="menu_ToolsPopup"
-# We have to use setTimeout() here to avoid a flickering menu bar when opening
-# the Tools menu, see bug 970769. This can be removed once we got rid of the
-# event loop spinning in Weave.Status._authManager.
-                         onpopupshowing="setTimeout(() => gSyncUI.updateUI());"
-                         >
+              <menupopup id="menu_ToolsPopup">
               <menuitem id="menu_openDownloads"
                         label="&downloads.label;"
                         accesskey="&downloads.accesskey;"
                         key="key_openDownloads"
                         command="Tools:Downloads"/>
               <menuitem id="menu_openAddons"
                         label="&addons.label;"
                         accesskey="&addons.accesskey;"
                         key="key_openAddons"
                         command="Tools:Addons"/>
 
               <!-- only one of sync-setup, sync-syncnowitem or sync-reauthitem will be showing at once -->
               <menuitem id="sync-setup"
                         label="&syncSignIn.label;"
                         accesskey="&syncSignIn.accesskey;"
                         observes="sync-setup-state"
-                        oncommand="gSyncUI.openPrefs('menubar')"/>
+                        oncommand="gSync.openPrefs('menubar')"/>
               <menuitem id="sync-syncnowitem"
                         label="&syncSyncNowItem.label;"
                         accesskey="&syncSyncNowItem.accesskey;"
                         observes="sync-syncnow-state"
-                        oncommand="gSyncUI.doSync(event);"/>
+                        oncommand="gSync.doSync(event);"/>
               <menuitem id="sync-reauthitem"
                         label="&syncReAuthItem.label;"
                         accesskey="&syncReAuthItem.accesskey;"
                         observes="sync-reauth-state"
-                        oncommand="gSyncUI.openSignInAgainPage('menubar');"/>
+                        oncommand="gSync.openSignInAgainPage('menubar');"/>
               <menuseparator id="devToolsSeparator"/>
               <menu id="webDeveloperMenu"
                     label="&webDeveloperMenu.label;"
                     accesskey="&webDeveloperMenu.accesskey;">
                 <menupopup id="menuWebDeveloperPopup">
                   <menuitem id="menu_pageSource"
                             observes="devtoolsMenuBroadcaster_PageSource"
                             accesskey="&pageSourceCmd.accesskey;"/>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-sync.js
@@ -0,0 +1,574 @@
+/* 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/. */
+
+Cu.import("resource://services-sync/UIState.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
+  "resource://gre/modules/FxAccountsWebChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+  "resource://services-sync/main.js");
+if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
+  XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+                                    "resource://gre/modules/CloudSync.jsm");
+}
+
+const MIN_STATUS_ANIMATION_DURATION = 1600;
+
+var gSync = {
+  _initialized: false,
+  // The last sync start time. Used to calculate the leftover animation time
+  // once syncing completes (bug 1239042).
+  _syncStartTime: 0,
+  _syncAnimationTimer: 0,
+
+  _obs: [
+    "weave:engine:sync:finish",
+    "quit-application",
+    UIState.ON_UPDATE
+  ],
+
+  get panelUIFooter() {
+    delete this.panelUIFooter;
+    return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
+  },
+
+  get panelUIStatus() {
+    delete this.panelUIStatus;
+    return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
+  },
+
+  get panelUIAvatar() {
+    delete this.panelUIAvatar;
+    return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
+  },
+
+  get panelUILabel() {
+    delete this.panelUILabel;
+    return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
+  },
+
+  get panelUIIcon() {
+    delete this.panelUIIcon;
+    return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
+  },
+
+  get fxaStrings() {
+    delete this.fxaStrings;
+    return this.fxaStrings = Services.strings.createBundle(
+      "chrome://browser/locale/accounts.properties"
+    );
+  },
+
+  get syncStrings() {
+    delete this.syncStrings;
+    // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+    //        but for now just make it work
+    return this.syncStrings = Services.strings.createBundle(
+      "chrome://weave/locale/sync.properties"
+    );
+  },
+
+  get sendTabToDeviceEnabled() {
+    return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+  },
+
+  get remoteClients() {
+    return Weave.Service.clientsEngine.remoteClients
+           .sort((a, b) => a.name.localeCompare(b.name));
+  },
+
+  init() {
+    // Bail out if we're already initialized or for pop-up windows.
+    if (this._initialized || !window.toolbar.visible) {
+      return;
+    }
+
+    for (let topic of this._obs) {
+      Services.obs.addObserver(this, topic, true);
+    }
+
+    // initial label for the sync buttons.
+    let broadcaster = document.getElementById("sync-status");
+    broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
+
+    // Update the UI
+    if (UIState.isReady()) {
+      const state = UIState.get();
+      // If we are not configured, the UI is already in the right state when
+      // we open the window. We can avoid a repaint.
+      if (state.status != UIState.STATUS_NOT_CONFIGURED) {
+        this.updateAllUI(state);
+      }
+    }
+
+    this.maybeMoveSyncedTabsButton();
+
+    EnsureFxAccountsWebChannel();
+
+    this._initialized = true;
+  },
+
+  uninit() {
+    if (!this._initialized) {
+      return;
+    }
+
+    for (let topic of this._obs) {
+      Services.obs.removeObserver(this, topic);
+    }
+
+    this._initialized = false;
+  },
+
+  observe(subject, topic, data) {
+    if (!this._initialized) {
+      Cu.reportError("browser-sync observer called after unload: " + topic);
+      return;
+    }
+    switch (topic) {
+      case UIState.ON_UPDATE:
+        const state = UIState.get();
+        this.updateAllUI(state);
+        break;
+      case "quit-application":
+        // Stop the animation timer on shutdown, since we can't update the UI
+        // after this.
+        clearTimeout(this._syncAnimationTimer);
+        break;
+      case "weave:engine:sync:finish":
+        if (data != "clients") {
+          return;
+        }
+        this.onClientsSynced();
+        break;
+    }
+  },
+
+  updateAllUI(state) {
+    this.updatePanelBadge(state);
+    this.updatePanelPopup(state);
+    this.updateStateBroadcasters(state);
+    this.updateSyncButtonsTooltip(state);
+    this.updateSyncStatus(state);
+  },
+
+  updatePanelPopup(state) {
+    let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
+    // The localization string is for the signed in text, but it's the default text as well
+    let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
+
+    const status = state.status;
+    // Reset the status bar to its original state.
+    this.panelUILabel.setAttribute("label", defaultLabel);
+    this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
+    this.panelUIFooter.removeAttribute("fxastatus");
+    this.panelUIAvatar.style.removeProperty("list-style-image");
+
+    if (status == UIState.STATUS_NOT_CONFIGURED) {
+      return;
+    }
+
+    // At this point we consider sync to be configured (but still can be in an error state).
+    if (status == UIState.STATUS_LOGIN_FAILED) {
+      let tooltipDescription = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
+      let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
+      this.panelUIFooter.setAttribute("fxastatus", "login-failed");
+      this.panelUILabel.setAttribute("label", errorLabel);
+      this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+      return;
+    } else if (status == UIState.STATUS_NOT_VERIFIED) {
+      let tooltipDescription = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
+      let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
+      this.panelUIFooter.setAttribute("fxastatus", "unverified");
+      this.panelUILabel.setAttribute("label", unverifiedLabel);
+      this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+      return;
+    }
+
+    // At this point we consider sync to be logged-in.
+    this.panelUIFooter.setAttribute("fxastatus", "signedin");
+    this.panelUILabel.setAttribute("label", state.displayName || state.email);
+
+    if (state.avatarURL) {
+      let bgImage = "url(\"" + state.avatarURL + "\")";
+      this.panelUIAvatar.style.listStyleImage = bgImage;
+
+      let img = new Image();
+      img.onerror = () => {
+        // Clear the image if it has trouble loading. Since this callback is asynchronous
+        // we check to make sure the image is still the same before we clear it.
+        if (this.panelUIAvatar.style.listStyleImage === bgImage) {
+          this.panelUIAvatar.style.removeProperty("list-style-image");
+        }
+      };
+      img.src = state.avatarURL;
+    }
+  },
+
+  updatePanelBadge(state) {
+    if (state.status == UIState.STATUS_LOGIN_FAILED ||
+        state.status == UIState.STATUS_NOT_VERIFIED) {
+      PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    } else {
+      PanelUI.removeNotification("fxa-needs-authentication");
+    }
+  },
+
+  updateStateBroadcasters(state) {
+    const status = state.status;
+
+    // Start off with a clean slate
+    document.getElementById("sync-reauth-state").hidden = true;
+    document.getElementById("sync-setup-state").hidden = true;
+    document.getElementById("sync-syncnow-state").hidden = true;
+
+    if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
+      document.getElementById("sync-syncnow-state").hidden = false;
+    } else if (status == UIState.STATUS_LOGIN_FAILED) {
+      // unhiding this element makes the menubar show the login failure state.
+      document.getElementById("sync-reauth-state").hidden = false;
+    } else if (status == UIState.STATUS_NOT_CONFIGURED ||
+               status == UIState.STATUS_NOT_VERIFIED) {
+      document.getElementById("sync-setup-state").hidden = false;
+    } else {
+      document.getElementById("sync-syncnow-state").hidden = false;
+    }
+  },
+
+  updateSyncStatus(state) {
+    const broadcaster = document.getElementById("sync-status");
+    const syncingUI = broadcaster.getAttribute("syncstatus") == "active";
+    if (state.syncing != syncingUI) { // Do we need to update the UI?
+      state.syncing ? this.onActivityStart() : this.onActivityStop();
+    }
+  },
+
+  onMenuPanelCommand() {
+    switch (this.panelUIFooter.getAttribute("fxastatus")) {
+    case "signedin":
+      this.openPrefs("menupanel");
+      break;
+    case "error":
+      if (this.panelUIFooter.getAttribute("fxastatus") == "unverified") {
+        this.openPrefs("menupanel");
+      } else {
+        this.openSignInAgainPage("menupanel");
+      }
+      break;
+    default:
+      this.openPrefs("menupanel");
+      break;
+    }
+
+    PanelUI.hide();
+  },
+
+  openAccountsPage(action, urlParams = {}) {
+    let params = new URLSearchParams();
+    if (action) {
+      params.set("action", action);
+    }
+    for (let name in urlParams) {
+      if (urlParams[name] !== undefined) {
+        params.set(name, urlParams[name]);
+      }
+    }
+    let url = "about:accounts?" + params;
+    switchToTabHavingURI(url, true, {
+      replaceQueryString: true
+    });
+  },
+
+  openSignInAgainPage(entryPoint) {
+    this.openAccountsPage("reauth", { entrypoint: entryPoint });
+  },
+
+  async openDevicesManagementPage(entryPoint) {
+    let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
+    switchToTabHavingURI(url, true, {
+      replaceQueryString: true
+    });
+  },
+
+  sendTabToDevice(url, clientId, title) {
+    Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
+  },
+
+  populateSendTabToDevicesMenu(devicesPopup, url, title) {
+    // remove existing menu items
+    while (devicesPopup.hasChildNodes()) {
+      devicesPopup.firstChild.remove();
+    }
+
+    const fragment = document.createDocumentFragment();
+
+    const onTargetDeviceCommand = (event) => {
+      let clients = event.target.getAttribute("clientId") ?
+        [event.target.getAttribute("clientId")] :
+        this.remoteClients.map(client => client.id);
+
+      clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+    }
+
+    function addTargetDevice(clientId, name) {
+      const targetDevice = document.createElement("menuitem");
+      targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+      targetDevice.setAttribute("class", "sendtab-target");
+      targetDevice.setAttribute("clientId", clientId);
+      targetDevice.setAttribute("label", name);
+      fragment.appendChild(targetDevice);
+    }
+
+    const clients = this.remoteClients;
+    for (let client of clients) {
+      addTargetDevice(client.id, client.name);
+    }
+
+    // "All devices" menu item
+    if (clients.length > 1) {
+      const separator = document.createElement("menuseparator");
+      fragment.appendChild(separator);
+      const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
+      addTargetDevice("", allDevicesLabel);
+    }
+
+    devicesPopup.appendChild(fragment);
+  },
+
+  isSendableURI(aURISpec) {
+    if (!aURISpec) {
+      return false;
+    }
+    // Disallow sending tabs with more than 65535 characters.
+    if (aURISpec.length > 65535) {
+      return false;
+    }
+    try {
+      // Filter out un-sendable URIs -- things like local files, object urls, etc.
+      const unsendableRegexp = new RegExp(
+        Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
+      return !unsendableRegexp.test(aURISpec);
+    } catch (e) {
+      // The preference has been removed, or is an invalid regexp, so we log an
+      // error and treat it as a valid URI -- and the more problematic case is
+      // the length, which we've already addressed.
+      Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
+      return true;
+    }
+  },
+
+  updateTabContextMenu(aPopupMenu, aTargetTab) {
+    if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
+      return;
+    }
+
+    const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
+    const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
+
+    ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
+    .forEach(id => { document.getElementById(id).hidden = !showSendTab });
+  },
+
+  initPageContextMenu(contextMenu) {
+    if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
+      return;
+    }
+
+    const remoteClientPresent = this.remoteClients.length > 0;
+    // showSendLink and showSendPage are mutually exclusive
+    let showSendLink = remoteClientPresent
+                       && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
+    const showSendPage = !showSendLink && remoteClientPresent
+                         && !(contextMenu.isContentSelected ||
+                              contextMenu.onImage || contextMenu.onCanvas ||
+                              contextMenu.onVideo || contextMenu.onAudio ||
+                              contextMenu.onLink || contextMenu.onTextInput)
+                         && this.isSendableURI(contextMenu.browser.currentURI.spec);
+
+    if (showSendLink) {
+      // This isn't part of the condition above since we don't want to try and
+      // send the page if a link is clicked on or selected but is not sendable.
+      showSendLink = this.isSendableURI(contextMenu.linkURL);
+    }
+
+    ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
+    .forEach(id => contextMenu.showItem(id, showSendPage));
+    ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
+    .forEach(id => contextMenu.showItem(id, showSendLink));
+  },
+
+  // Functions called by observers
+  onActivityStart() {
+    clearTimeout(this._syncAnimationTimer);
+    this._syncStartTime = Date.now();
+
+    let broadcaster = document.getElementById("sync-status");
+    broadcaster.setAttribute("syncstatus", "active");
+    broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncing2.label"));
+    broadcaster.setAttribute("disabled", "true");
+  },
+
+  _onActivityStop() {
+    if (!gBrowser)
+      return;
+    let broadcaster = document.getElementById("sync-status");
+    broadcaster.removeAttribute("syncstatus");
+    broadcaster.removeAttribute("disabled");
+    broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
+    Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
+  },
+
+  onActivityStop() {
+    let now = Date.now();
+    let syncDuration = now - this._syncStartTime;
+
+    if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
+      let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
+      clearTimeout(this._syncAnimationTimer);
+      this._syncAnimationTimer = setTimeout(() => this._onActivityStop(), animationTime);
+    } else {
+      this._onActivityStop();
+    }
+  },
+
+  // doSync forces a sync - it *does not* return a promise as it is called
+  // via the various UI components.
+  doSync() {
+    if (!UIState.isReady()) {
+      return;
+    }
+    const state = UIState.get();
+    if (state.status == UIState.STATUS_SIGNED_IN) {
+      setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
+    }
+    Services.obs.notifyObservers(null, "cloudsync:user-sync");
+  },
+
+  openPrefs(entryPoint = "syncbutton") {
+    window.openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
+  },
+
+  openSyncedTabsPanel() {
+    let placement = CustomizableUI.getPlacementOfWidget("sync-button");
+    let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
+    let anchor = document.getElementById("sync-button") ||
+                 document.getElementById("PanelUI-menu-button");
+    if (area == CustomizableUI.AREA_PANEL) {
+      // The button is in the panel, so we need to show the panel UI, then our
+      // subview.
+      PanelUI.show().then(() => {
+        PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+      }).catch(Cu.reportError);
+    } else {
+      // It is placed somewhere else - just try and show it.
+      PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+    }
+  },
+
+  /* After we are initialized we perform a once-only check for the sync
+     button being in "customize purgatory" and if so, move it to the panel.
+     This is done primarily for profiles created before SyncedTabs landed,
+     where the button defaulted to being in that purgatory.
+     We use a preference to ensure we only do it once, so people can still
+     customize it away and have it stick.
+  */
+  maybeMoveSyncedTabsButton() {
+    const prefName = "browser.migrated-sync-button";
+    let migrated = Services.prefs.getBoolPref(prefName, false);
+    if (migrated) {
+      return;
+    }
+    if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
+      CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+    }
+    Services.prefs.setBoolPref(prefName, true);
+  },
+
+  /* Update the tooltip for the sync-status broadcaster (which will update the
+     Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
+     If Sync is configured, the tooltip is when the last sync occurred,
+     otherwise the tooltip reflects the fact that Sync needs to be
+     (re-)configured.
+  */
+  updateSyncButtonsTooltip(state) {
+    const status = state.status;
+
+    // This is a little messy as the Sync buttons are 1/2 Sync related and
+    // 1/2 FxA related - so for some strings we use Sync strings, but for
+    // others we reach into gSync for strings.
+    let tooltiptext;
+    if (status == UIState.STATUS_NOT_VERIFIED) {
+      // "needs verification"
+      tooltiptext = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
+    } else if (status == UIState.STATUS_NOT_CONFIGURED) {
+      // "needs setup".
+      tooltiptext = this.syncStrings.GetStringFromName("signInToSync.description");
+    } else if (status == UIState.STATUS_LOGIN_FAILED) {
+      // "need to reconnect/re-enter your password"
+      tooltiptext = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
+    } else {
+      // Sync appears configured - format the "last synced at" time.
+      tooltiptext = this.formatLastSyncDate(state.lastSync);
+    }
+
+    let broadcaster = document.getElementById("sync-status");
+    if (broadcaster) {
+      if (tooltiptext) {
+        broadcaster.setAttribute("tooltiptext", tooltiptext);
+      } else {
+        broadcaster.removeAttribute("tooltiptext");
+      }
+    }
+  },
+
+  get withinLastWeekFormat() {
+    delete this.withinLastWeekFormat;
+    return this.withinLastWeekFormat = new Intl.DateTimeFormat(undefined,
+      {weekday: "long", hour: "numeric", minute: "numeric"});
+  },
+
+  get oneWeekOrOlderFormat() {
+    delete this.oneWeekOrOlderFormat;
+    return this.oneWeekOrOlderFormat = new Intl.DateTimeFormat(undefined,
+      {month: "long", day: "numeric"});
+  },
+
+  formatLastSyncDate(date) {
+    let sixDaysAgo = (() => {
+      let tempDate = new Date();
+      tempDate.setDate(tempDate.getDate() - 6);
+      tempDate.setHours(0, 0, 0, 0);
+      return tempDate;
+    })();
+
+    // It may be confusing for the user to see "Last Sync: Monday" when the last
+    // sync was indeed a Monday, but 3 weeks ago.
+    let dateFormat = date < sixDaysAgo ? this.oneWeekOrOlderFormat : this.withinLastWeekFormat;
+
+    let lastSyncDateString = dateFormat.format(date);
+    return this.syncStrings.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
+  },
+
+  onClientsSynced() {
+    let broadcaster = document.getElementById("sync-syncnow-state");
+    if (broadcaster) {
+      if (Weave.Service.clientsEngine.stats.numClients > 1) {
+        broadcaster.setAttribute("devices-status", "multi");
+      } else {
+        broadcaster.setAttribute("devices-status", "single");
+      }
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference
+  ])
+};
+
+XPCOMUtils.defineLazyGetter(gSync, "weaveService", function() {
+  return Components.classes["@mozilla.org/weave/service;1"]
+                   .getService(Components.interfaces.nsISupports)
+                   .wrappedJSObject;
+});
deleted file mode 100644
--- a/browser/base/content/browser-syncui.js
+++ /dev/null
@@ -1,498 +0,0 @@
-/* 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/. */
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
-  XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
-                                    "resource://gre/modules/CloudSync.jsm");
-}
-
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
-                                  "resource://gre/modules/FxAccounts.jsm");
-
-const MIN_STATUS_ANIMATION_DURATION = 1600;
-
-// gSyncUI handles updating the tools menu and displaying notifications.
-var gSyncUI = {
-  _obs: ["weave:service:sync:start",
-         "weave:service:sync:finish",
-         "weave:service:sync:error",
-         "weave:service:setup-complete",
-         "weave:service:login:start",
-         "weave:service:login:finish",
-         "weave:service:login:error",
-         "weave:service:logout:finish",
-         "weave:service:start-over",
-         "weave:service:start-over:finish",
-         "weave:ui:login:error",
-         "weave:ui:sync:error",
-         "weave:ui:sync:finish",
-         "weave:ui:clear-error",
-         "weave:engine:sync:finish"
-  ],
-
-  _unloaded: false,
-  // The last sync start time. Used to calculate the leftover animation time
-  // once syncing completes (bug 1239042).
-  _syncStartTime: 0,
-  _syncAnimationTimer: 0,
-  _withinLastWeekFormat: null,
-  _oneWeekOrOlderFormat: null,
-
-  init() {
-    // Proceed to set up the UI if Sync has already started up.
-    // Otherwise we'll do it when Sync is firing up.
-    if (this.weaveService.ready) {
-      this.initUI();
-      return;
-    }
-
-    // Sync isn't ready yet, but we can still update the UI with an initial
-    // state - we haven't called initUI() yet, but that's OK - that's more
-    // about observers for state changes, and will be called once Sync is
-    // ready to start sending notifications.
-    this.updateUI();
-
-    Services.obs.addObserver(this, "weave:service:ready", true);
-    Services.obs.addObserver(this, "quit-application", true);
-
-    // Remove the observer if the window is closed before the observer
-    // was triggered.
-    window.addEventListener("unload", () => {
-      this._unloaded = true;
-      Services.obs.removeObserver(this, "weave:service:ready");
-      Services.obs.removeObserver(this, "quit-application");
-
-      if (this.weaveService.ready) {
-        this._obs.forEach(topic => {
-          Services.obs.removeObserver(this, topic);
-        });
-      }
-    }, { once: true });
-  },
-
-  initUI: function SUI_initUI() {
-    // If this is a browser window?
-    if (gBrowser) {
-      this._obs.push("weave:notification:added");
-    }
-
-    this._obs.forEach(function(topic) {
-      Services.obs.addObserver(this, topic, true);
-    }, this);
-
-    // initial label for the sync buttons.
-    let broadcaster = document.getElementById("sync-status");
-    broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
-
-    this.maybeMoveSyncedTabsButton();
-
-    this.updateUI();
-  },
-
-
-  // Returns a promise that resolves with true if Sync needs to be configured,
-  // false otherwise.
-  _needsSetup() {
-    return fxAccounts.getSignedInUser().then(user => {
-      // We want to treat "account needs verification" as "needs setup".
-      return !(user && user.verified);
-    });
-  },
-
-  // Returns a promise that resolves with true if the user currently signed in
-  // to Sync needs to be verified, false otherwise.
-  _needsVerification() {
-    return fxAccounts.getSignedInUser().then(user => {
-      // If there is no user, they can't be in a "needs verification" state.
-      if (!user) {
-        return false;
-      }
-      return !user.verified;
-    });
-  },
-
-  // Note that we don't show login errors in a notification bar here, but do
-  // still need to track a login-failed state so the "Tools" menu updates
-  // with the correct state.
-  loginFailed() {
-    // If Sync isn't already ready, we don't want to force it to initialize
-    // by referencing Weave.Status - and it isn't going to be accurate before
-    // Sync is ready anyway.
-    if (!this.weaveService.ready) {
-      this.log.debug("loginFailed has sync not ready, so returning false");
-      return false;
-    }
-    this.log.debug("loginFailed has sync state=${sync}",
-                   { sync: Weave.Status.login});
-    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
-  },
-
-  // Kick off an update of the UI - does *not* return a promise.
-  updateUI() {
-    this._promiseUpdateUI().catch(err => {
-      this.log.error("updateUI failed", err);
-    })
-  },
-
-  // Updates the UI - returns a promise.
-  _promiseUpdateUI() {
-    return this._needsSetup().then(needsSetup => {
-      if (!gBrowser)
-        return Promise.resolve();
-
-      let loginFailed = this.loginFailed();
-
-      // Start off with a clean slate
-      document.getElementById("sync-reauth-state").hidden = true;
-      document.getElementById("sync-setup-state").hidden = true;
-      document.getElementById("sync-syncnow-state").hidden = true;
-
-      if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
-        document.getElementById("sync-syncnow-state").hidden = false;
-      } else if (loginFailed) {
-        // unhiding this element makes the menubar show the login failure state.
-        document.getElementById("sync-reauth-state").hidden = false;
-      } else if (needsSetup) {
-        document.getElementById("sync-setup-state").hidden = false;
-      } else {
-        document.getElementById("sync-syncnow-state").hidden = false;
-      }
-
-      return this._updateSyncButtonsTooltip();
-    });
-  },
-
-  // Functions called by observers
-  onActivityStart() {
-    if (!gBrowser)
-      return;
-
-    this.log.debug("onActivityStart");
-
-    clearTimeout(this._syncAnimationTimer);
-    this._syncStartTime = Date.now();
-
-    let broadcaster = document.getElementById("sync-status");
-    broadcaster.setAttribute("syncstatus", "active");
-    broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label"));
-    broadcaster.setAttribute("disabled", "true");
-
-    this.updateUI();
-  },
-
-  _updateSyncStatus() {
-    if (!gBrowser)
-      return;
-    let broadcaster = document.getElementById("sync-status");
-    broadcaster.removeAttribute("syncstatus");
-    broadcaster.removeAttribute("disabled");
-    broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
-    this.updateUI();
-  },
-
-  onActivityStop() {
-    if (!gBrowser)
-      return;
-    this.log.debug("onActivityStop");
-
-    let now = Date.now();
-    let syncDuration = now - this._syncStartTime;
-
-    if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
-      let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
-      clearTimeout(this._syncAnimationTimer);
-      this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime);
-    } else {
-      this._updateSyncStatus();
-    }
-  },
-
-  onLoginError: function SUI_onLoginError() {
-    this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
-
-    // We don't show any login errors here; browser-fxaccounts shows them in
-    // the hamburger menu.
-    this.updateUI();
-  },
-
-  onLogout: function SUI_onLogout() {
-    this.updateUI();
-  },
-
-  _getAppName() {
-    let brand = Services.strings.createBundle(
-      "chrome://branding/locale/brand.properties");
-    return brand.GetStringFromName("brandShortName");
-  },
-
-  // Commands
-  // doSync forces a sync - it *does not* return a promise as it is called
-  // via the various UI components.
-  doSync() {
-    this._needsSetup().then(needsSetup => {
-      if (!needsSetup) {
-        setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
-      }
-      Services.obs.notifyObservers(null, "cloudsync:user-sync");
-    }).catch(err => {
-      this.log.error("Failed to force a sync", err);
-    });
-  },
-
-  // Handle clicking the toolbar button - which either opens the Sync setup
-  // pages or forces a sync now. Does *not* return a promise as it is called
-  // via the UI.
-  handleToolbarButton() {
-    this._needsSetup().then(needsSetup => {
-      if (needsSetup || this.loginFailed()) {
-        this.openPrefs();
-      } else {
-        this.doSync();
-      }
-    }).catch(err => {
-      this.log.error("Failed to handle toolbar button command", err);
-    });
-  },
-
-  /**
-   * Open the Sync preferences.
-   *
-   * @param entryPoint
-   *        Indicates the entrypoint from where this method was called.
-   */
-  openPrefs(entryPoint = "syncbutton") {
-    openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
-  },
-
-  openSignInAgainPage(entryPoint = "syncbutton") {
-    gFxAccounts.openSignInAgainPage(entryPoint);
-  },
-
-  openSyncedTabsPanel() {
-    let placement = CustomizableUI.getPlacementOfWidget("sync-button");
-    let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
-    let anchor = document.getElementById("sync-button") ||
-                 document.getElementById("PanelUI-menu-button");
-    if (area == CustomizableUI.AREA_PANEL) {
-      // The button is in the panel, so we need to show the panel UI, then our
-      // subview.
-      PanelUI.show().then(() => {
-        PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
-      }).catch(Cu.reportError);
-    } else {
-      // It is placed somewhere else - just try and show it.
-      PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
-    }
-  },
-
-  /* After Sync is initialized we perform a once-only check for the sync
-     button being in "customize purgatory" and if so, move it to the panel.
-     This is done primarily for profiles created before SyncedTabs landed,
-     where the button defaulted to being in that purgatory.
-     We use a preference to ensure we only do it once, so people can still
-     customize it away and have it stick.
-  */
-  maybeMoveSyncedTabsButton() {
-    const prefName = "browser.migrated-sync-button";
-    let migrated = Services.prefs.getBoolPref(prefName, false);
-    if (migrated) {
-      return;
-    }
-    if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
-      CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
-    }
-    Services.prefs.setBoolPref(prefName, true);
-  },
-
-  /* Update the tooltip for the sync-status broadcaster (which will update the
-     Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
-     If Sync is configured, the tooltip is when the last sync occurred,
-     otherwise the tooltip reflects the fact that Sync needs to be
-     (re-)configured.
-  */
-  _updateSyncButtonsTooltip: Task.async(function* () {
-    if (!gBrowser)
-      return;
-
-    let email;
-    let user = yield fxAccounts.getSignedInUser();
-    if (user) {
-      email = user.email;
-    }
-
-    let needsSetup = yield this._needsSetup();
-    let needsVerification = yield this._needsVerification();
-    let loginFailed = this.loginFailed();
-    // This is a little messy as the Sync buttons are 1/2 Sync related and
-    // 1/2 FxA related - so for some strings we use Sync strings, but for
-    // others we reach into gFxAccounts for strings.
-    let tooltiptext;
-    if (needsVerification) {
-      // "needs verification"
-      tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1);
-    } else if (needsSetup) {
-      // "needs setup".
-      tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description");
-    } else if (loginFailed) {
-      // "need to reconnect/re-enter your password"
-      tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1);
-    } else {
-      // Sync appears configured - format the "last synced at" time.
-      try {
-        let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
-        tooltiptext = this.formatLastSyncDate(lastSync);
-      } catch (e) {
-        // pref doesn't exist (which will be the case until we've seen the
-        // first successful sync) or is invalid (which should be impossible!)
-        // Just leave tooltiptext as the empty string in these cases, which
-        // will cause the tooltip to be removed below.
-      }
-    }
-
-    // We've done all our promise-y work and ready to update the UI - make
-    // sure it hasn't been torn down since we started.
-    if (!gBrowser)
-      return;
-
-    let broadcaster = document.getElementById("sync-status");
-    if (broadcaster) {
-      if (tooltiptext) {
-        broadcaster.setAttribute("tooltiptext", tooltiptext);
-      } else {
-        broadcaster.removeAttribute("tooltiptext");
-      }
-    }
-  }),
-
-  getWithinLastWeekFormat() {
-    return this._withinLastWeekFormat ||
-           (this._withinLastWeekFormat =
-             new Intl.DateTimeFormat(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
-  },
-
-  getOneWeekOrOlderFormat() {
-    return this._oneWeekOrOlderFormat ||
-           (this._oneWeekOrOlderFormat =
-             new Intl.DateTimeFormat(undefined, {month: "long", day: "numeric"}));
-  },
-
-  formatLastSyncDate(date) {
-    let sixDaysAgo = (() => {
-      let tempDate = new Date();
-      tempDate.setDate(tempDate.getDate() - 6);
-      tempDate.setHours(0, 0, 0, 0);
-      return tempDate;
-    })();
-
-    // It may be confusing for the user to see "Last Sync: Monday" when the last
-    // sync was indeed a Monday, but 3 weeks ago.
-    let dateFormat = date < sixDaysAgo ? this.getOneWeekOrOlderFormat() : this.getWithinLastWeekFormat();
-
-    let lastSyncDateString = dateFormat.format(date);
-    return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
-  },
-
-  onClientsSynced() {
-    let broadcaster = document.getElementById("sync-syncnow-state");
-    if (broadcaster) {
-      if (Weave.Service.clientsEngine.stats.numClients > 1) {
-        broadcaster.setAttribute("devices-status", "multi");
-      } else {
-        broadcaster.setAttribute("devices-status", "single");
-      }
-    }
-  },
-
-  observe: function SUI_observe(subject, topic, data) {
-    this.log.debug("observed", topic);
-    if (this._unloaded) {
-      Cu.reportError("SyncUI observer called after unload: " + topic);
-      return;
-    }
-
-    // Unwrap, just like Svc.Obs, but without pulling in that dependency.
-    if (subject && typeof subject == "object" &&
-        ("wrappedJSObject" in subject) &&
-        ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
-      subject = subject.wrappedJSObject.object;
-    }
-
-    // First handle "activity" only.
-    switch (topic) {
-      case "weave:service:sync:start":
-        this.onActivityStart();
-        break;
-      case "weave:service:sync:finish":
-      case "weave:service:sync:error":
-        this.onActivityStop();
-        break;
-    }
-    // Now non-activity state (eg, enabled, errors, etc)
-    // Note that sync uses the ":ui:" notifications for errors because sync.
-    switch (topic) {
-      case "weave:ui:sync:finish":
-        // Do nothing.
-        break;
-      case "weave:ui:sync:error":
-      case "weave:service:setup-complete":
-      case "weave:service:login:finish":
-      case "weave:service:login:start":
-      case "weave:service:start-over":
-        this.updateUI();
-        break;
-      case "weave:ui:login:error":
-      case "weave:service:login:error":
-        this.onLoginError();
-        break;
-      case "weave:service:logout:finish":
-        this.onLogout();
-        break;
-      case "weave:service:start-over:finish":
-        this.updateUI();
-        break;
-      case "weave:service:ready":
-        this.initUI();
-        break;
-      case "weave:notification:added":
-        this.initNotifications();
-        break;
-      case "weave:engine:sync:finish":
-        if (data != "clients") {
-          return;
-        }
-        this.onClientsSynced();
-        break;
-      case "quit-application":
-        // Stop the animation timer on shutdown, since we can't update the UI
-        // after this.
-        clearTimeout(this._syncAnimationTimer);
-        break;
-    }
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIObserver,
-    Ci.nsISupportsWeakReference
-  ])
-};
-
-XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
-  // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
-  //        but for now just make it work
-  return Services.strings.createBundle(
-    "chrome://weave/locale/sync.properties");
-});
-
-XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
-  return Log.repository.getLogger("browserwindow.syncui");
-});
-
-XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() {
-  return Components.classes["@mozilla.org/weave/service;1"]
-                   .getService(Components.interfaces.nsISupports)
-                   .wrappedJSObject;
-});
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1498,18 +1498,17 @@ var gBrowserInit = {
 
     if (Win7Features)
       Win7Features.onOpenWindow();
 
     FullScreen.init();
     PointerLock.init();
 
     // initialize the sync UI
-    gSyncUI.init();
-    gFxAccounts.init();
+    gSync.init();
 
     if (AppConstants.MOZ_DATA_REPORTING)
       gDataNotificationInfoBar.init();
 
     gBrowserThumbnails.init();
 
     gMenuButtonUpdateBadge.init();
 
@@ -1646,17 +1645,17 @@ var gBrowserInit = {
     CombinedStopReload.uninit();
 
     gGestureSupport.init(false);
 
     gHistorySwipeAnimation.uninit();
 
     FullScreen.uninit();
 
-    gFxAccounts.uninit();
+    gSync.uninit();
 
     gExtensionsNotifications.uninit();
 
     Services.obs.removeObserver(gPluginHandler.NPAPIPluginCrashed, "plugin-crashed");
 
     try {
       gBrowser.removeProgressListener(window.XULBrowserWindow);
       gBrowser.removeTabsProgressListener(window.TabsProgressListener);
@@ -1809,17 +1808,17 @@ if (AppConstants.platform == "macosx") {
 
     // initialise the offline listener
     BrowserOffline.init();
 
     // initialize the private browsing UI
     gPrivateBrowsingUI.init();
 
     // initialize the sync UI
-    gSyncUI.init();
+    gSync.init();
 
     if (AppConstants.E10S_TESTING_ONLY) {
       gRemoteTabsUI.init();
     }
   };
 
   gBrowserInit.nonBrowserWindowShutdown = function() {
     let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"]
@@ -6794,17 +6793,17 @@ function checkEmptyPageOrigin(browser = 
   }
   // ... so for those that don't have them, enforce that the page has the
   // system principal (this matches e.g. on about:newtab).
   let ssm = Services.scriptSecurityManager;
   return ssm.isSystemPrincipal(contentPrincipal);
 }
 
 function BrowserOpenSyncTabs() {
-  gSyncUI.openSyncedTabsPanel();
+  gSync.openSyncedTabsPanel();
 }
 
 function ReportFalseDeceptiveSite() {
   let docURI = gBrowser.selectedBrowser.documentURI;
   let isPhishingPage =
     docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
 
   if (isPhishingPage) {
@@ -8168,17 +8167,17 @@ var TabContextMenu = {
     }
 
     this.contextTab.toggleMuteMenuItem = toggleMute;
     this._updateToggleMuteMenuItem(this.contextTab);
 
     this.contextTab.addEventListener("TabAttrModified", this);
     aPopupMenu.addEventListener("popuphiding", this);
 
-    gFxAccounts.updateTabContextMenu(aPopupMenu, this.contextTab);
+    gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
   },
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "popuphiding":
         gBrowser.removeEventListener("TabAttrModified", this);
         aEvent.target.removeEventListener("popuphiding", this);
         break;
       case "TabAttrModified":
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -101,17 +101,17 @@
                 tbattr="tabbrowser-remote"
                 hidden="true"
                 oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
 #endif
       <menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
       <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
             accesskey="&sendTabToDevice.accesskey;" hidden="true">
         <menupopup id="context_sendTabToDevicePopupMenu"
-                   onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
+                   onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
       </menu>
       <menuseparator/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple-visible"
                 oncommand="gBrowser.reloadAllTabs();"/>
       <menuitem id="context_bookmarkAllTabs"
                 label="&bookmarkAllTabs.label;"
                 accesskey="&bookmarkAllTabs.accesskey;"
@@ -487,17 +487,17 @@
                 id="syncedTabsCopySelected"/>
       <menuseparator/>
       <menuitem label="&syncedTabs.context.openAllInTabs.label;"
                 accesskey="&syncedTabs.context.openAllInTabs.accesskey;"
                 id="syncedTabsOpenAllInTabs"/>
       <menuitem label="&syncedTabs.context.managedevices.label;"
                 accesskey="&syncedTabs.context.managedevices.accesskey;"
                 id="syncedTabsManageDevices"
-                oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-sidebar');"/>
+                oncommand="gSync.openDevicesManagementPage('syncedtabs-sidebar');"/>
       <menuitem label="&syncSyncNowItem.label;"
                 accesskey="&syncSyncNowItem.accesskey;"
                 id="syncedTabsRefresh"/>
     </menupopup>
     <menupopup id="SyncedTabsSidebarTabsFilterContext"
                class="textbox-contextmenu">
       <menuitem label="&undoCmd.label;"
                 accesskey="&undoCmd.accesskey;"
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -25,18 +25,16 @@
 <script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-plugins.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-refreshblocker.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
-<script type="application/javascript" src="chrome://browser/content/browser-syncui.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-thumbnails.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/>
 
 #ifdef MOZ_DATA_REPORTING
 <script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/>
 #endif
-
-<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
--- a/browser/base/content/moz.build
+++ b/browser/base/content/moz.build
@@ -116,19 +116,16 @@ with Files("browser-customization.js"):
     BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
 
 with Files("browser-feeds.js"):
     BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
 
 with Files("browser-fullZoom.js"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browsing")
 
-with Files("browser-fxaccounts.js"):
-    BUG_COMPONENT = ("Core", "FxAccounts")
-
 with Files("browser-gestureSupport.js"):
     BUG_COMPONENT = ("Core", "Widget: Cocoa")
 
 with Files("browser-media.js"):
     BUG_COMPONENT = ("Core", "Audio/Video: Playback")
 
 with Files("browser-places.js"):
     BUG_COMPONENT = ("Firefox", "Bookmarks & History")
@@ -140,17 +137,17 @@ with Files("browser-refreshblocker.js"):
     BUG_COMPONENT = ("Firefox", "Disability Access")
 
 with Files("browser-safebrowsing.js"):
     BUG_COMPONENT = ("Toolkit", "Safe Browsing")
 
 with Files("*social*"):
     BUG_COMPONENT = ("Firefox", "SocialAPI")
 
-with Files("browser-syncui.js"):
+with Files("browser-sync.js"):
     BUG_COMPONENT = ("Firefox", "Sync")
 
 with Files("browser-tabPreviews.xml"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
 with Files("contentSearch*"):
     BUG_COMPONENT = ("Firefox", "Search")
 
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -596,17 +596,17 @@ nsContextMenu.prototype = {
       return;
     }
     let popup = document.getElementById("fill-login-popup");
     let insertBeforeElement = document.getElementById("fill-login-no-logins");
     popup.insertBefore(fragment, insertBeforeElement);
   },
 
   initSyncItems() {
-    gFxAccounts.initPageContextMenu(this);
+    gSync.initPageContextMenu(this);
   },
 
   openPasswordManager() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode() {
     let gBrowser = this.browser.ownerGlobal.gBrowser;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -3,27 +3,25 @@
 # TRY ONE OF THE MORE TOPICAL SIBLING DIRECTORIES.                            #
 # THIS DIRECTORY HAS 200+ TESTS AND TAKES AGES TO RUN ON A DEBUG BUILD.       #
 # PLEASE, FOR THE LOVE OF WHATEVER YOU HOLD DEAR, DO NOT ADD MORE TESTS HERE. #
 ###############################################################################
 
 [DEFAULT]
 support-files =
   POSTSearchEngine.xml
-  accounts_testRemoteCommands.html
   alltabslistener.html
   app_bug575561.html
   app_subframe_bug575561.html
   aboutHome_content_script.js
   audio.ogg
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
   browser_bug970746.xhtml
-  browser_fxa_web_channel.html
   browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   browser_web_channel_iframe.html
   bug592338.html
   bug792517-2.html
   bug792517.html
@@ -88,21 +86,16 @@ support-files =
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
   !/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
   !/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
 
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutAccounts.js]
-skip-if = os == "linux" # Bug 958026
-support-files =
-  content_aboutAccounts.js
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_aboutCertError.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_aboutNetError.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_aboutSupport.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_aboutSupport_newtab_security_state.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
@@ -370,21 +363,16 @@ skip-if = true # browser_drag.js is disa
 [browser_findbarClose.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_focusonkeydown.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_fullscreen-window-open.js]
 tags = fullscreen
 skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_fxaccounts.js]
-support-files = fxa_profile_handler.sjs
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_fxa_web_channel.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_gestureSupport.js]
 skip-if = e10s # Bug 863514 - no gesture support.
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_getshortcutoruri.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_hide_removing.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_homeDrop.js]
@@ -491,19 +479,16 @@ support-files =
 [browser_ssl_error_reports.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_star_hsts.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_storagePressure_notification.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_subframe_favicons_not_used.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_syncui.js]
-skip-if = os == 'linux' # Bug 1304272
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_close_dependent_window.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tabDrop.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tabReorder.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_detach_restore.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
deleted file mode 100644
--- a/browser/base/content/test/general/browser_aboutAccounts.js
+++ /dev/null
@@ -1,487 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed.
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-  "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-  "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
-  "resource://gre/modules/FxAccounts.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-  "resource://gre/modules/FileUtils.jsm");
-
-const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
-// Preference helpers.
-var changedPrefs = new Set();
-
-function setPref(name, value) {
-  changedPrefs.add(name);
-  Services.prefs.setCharPref(name, value);
-}
-
-registerCleanupFunction(function() {
-  // Ensure we don't pollute prefs for next tests.
-  for (let name of changedPrefs) {
-    Services.prefs.clearUserPref(name);
-  }
-});
-
-var gTests = [
-{
-  desc: "Test the remote commands",
-  *teardown() {
-    gBrowser.removeCurrentTab();
-    yield signOut();
-  },
-  *run() {
-    setPref("identity.fxaccounts.remote.signup.uri",
-            "https://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
-    let tab = yield promiseNewTabLoadEvent("about:accounts");
-    let mm = tab.linkedBrowser.messageManager;
-
-    let deferred = Promise.defer();
-
-    // We'll get a message when openPrefs() is called, which this test should
-    // arrange.
-    let promisePrefsOpened = promiseOneMessage(tab, "test:openPrefsCalled");
-    let results = 0;
-    try {
-      mm.addMessageListener("test:response", function responseHandler(msg) {
-        let data = msg.data.data;
-        if (data.type == "testResult") {
-          ok(data.pass, data.info);
-          results++;
-        } else if (data.type == "testsComplete") {
-          is(results, data.count, "Checking number of results received matches the number of tests that should have run");
-          mm.removeMessageListener("test:response", responseHandler);
-          deferred.resolve();
-        }
-      });
-    } catch (e) {
-      ok(false, "Failed to get all commands");
-      deferred.reject();
-    }
-    yield deferred.promise;
-    yield promisePrefsOpened;
-  }
-},
-{
-  desc: "Test action=signin - no user logged in",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    // When this loads with no user logged-in, we expect the "normal" URL
-    const expected_url = "https://example.com/?is_sign_in";
-    setPref("identity.fxaccounts.remote.signin.uri", expected_url);
-    let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
-    is(url, expected_url, "action=signin got the expected URL");
-    // we expect the remote iframe to be shown.
-    yield checkVisibilities(tab, {
-      stage: false, // parent of 'manage' and 'intro'
-      manage: false,
-      intro: false, // this is  "get started"
-      remote: true,
-      networkError: false
-    });
-  }
-},
-{
-  desc: "Test action=signin - user logged in",
-  *teardown() {
-    gBrowser.removeCurrentTab();
-    yield signOut();
-  },
-  *run() {
-    // When this loads with a user logged-in, we expect the normal URL to
-    // have been ignored and the "manage" page to be shown.
-    const expected_url = "https://example.com/?is_sign_in";
-    setPref("identity.fxaccounts.remote.signin.uri", expected_url);
-    yield setSignedInUser();
-    let tab = yield promiseNewTabLoadEvent("about:accounts?action=signin");
-    // about:accounts initializes after fetching the current user from Fxa -
-    // so we also request it - by the time we get it we know it should have
-    // done its thing.
-    yield fxAccounts.getSignedInUser();
-    // we expect "manage" to be shown.
-    yield checkVisibilities(tab, {
-      stage: true, // parent of 'manage' and 'intro'
-      manage: true,
-      intro: false, // this is  "get started"
-      remote: false,
-      networkError: false
-    });
-  }
-},
-{
-  desc: "Test action=signin - captive portal",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    const signinUrl = "https://redirproxy.example.com/test";
-    setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
-    let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
-    yield checkVisibilities(tab, {
-      stage: true, // parent of 'manage' and 'intro'
-      manage: false,
-      intro: false, // this is  "get started"
-      remote: false,
-      networkError: true
-    });
-  }
-},
-{
-  desc: "Test action=signin - offline",
-  teardown: () => {
-    gBrowser.removeCurrentTab();
-    BrowserOffline.toggleOfflineStatus();
-  },
-  *run() {
-    BrowserOffline.toggleOfflineStatus();
-    Services.cache2.clear();
-
-    const signinUrl = "https://unknowndomain.cow";
-    setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
-    let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
-    yield checkVisibilities(tab, {
-      stage: true, // parent of 'manage' and 'intro'
-      manage: false,
-      intro: false, // this is  "get started"
-      remote: false,
-      networkError: true
-    });
-  }
-},
-{
-  desc: "Test action=signup - no user logged in",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    const expected_url = "https://example.com/?is_sign_up";
-    setPref("identity.fxaccounts.remote.signup.uri", expected_url);
-    let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signup");
-    is(url, expected_url, "action=signup got the expected URL");
-    // we expect the remote iframe to be shown.
-    yield checkVisibilities(tab, {
-      stage: false, // parent of 'manage' and 'intro'
-      manage: false,
-      intro: false, // this is  "get started"
-      remote: true,
-      networkError: false
-    });
-  },
-},
-{
-  desc: "Test action=signup - user logged in",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    const expected_url = "https://example.com/?is_sign_up";
-    setPref("identity.fxaccounts.remote.signup.uri", expected_url);
-    yield setSignedInUser();
-    let tab = yield promiseNewTabLoadEvent("about:accounts?action=signup");
-    yield fxAccounts.getSignedInUser();
-    // we expect "manage" to be shown.
-    yield checkVisibilities(tab, {
-      stage: true, // parent of 'manage' and 'intro'
-      manage: true,
-      intro: false, // this is  "get started"
-      remote: false,
-      networkError: false
-    });
-  },
-},
-{
-  desc: "Test action=reauth",
-  *teardown() {
-    gBrowser.removeCurrentTab();
-    yield signOut();
-  },
-  *run() {
-    const expected_url = "https://example.com/force_auth";
-    setPref("identity.fxaccounts.remote.force_auth.uri", expected_url);
-
-    yield setSignedInUser();
-    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
-    // The current user will be appended to the url
-    let expected = expected_url + "?uid=1234%40lcip.org&email=foo%40example.com";
-    is(url, expected, "action=reauth got the expected URL");
-  },
-},
-{
-  desc: "Test with migrateToDevEdition enabled (success)",
-  *teardown() {
-    gBrowser.removeCurrentTab();
-    yield signOut();
-  },
-  *run() {
-    let fxAccountsCommon = {};
-    Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
-    const pref = "identity.fxaccounts.migrateToDevEdition";
-    changedPrefs.add(pref);
-    Services.prefs.setBoolPref(pref, true);
-
-    // Create the signedInUser.json file that will be used as the source of
-    // migrated user data.
-    let signedInUser = {
-      version: 1,
-      accountData: {
-        email: "foo@example.com",
-        uid: "1234@lcip.org",
-        sessionToken: "dead",
-        verified: true
-      }
-    };
-    // We use a sub-dir of the real profile dir as the "pretend" profile dir
-    // for this test.
-    let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
-    let mockDir = profD.clone();
-    mockDir.append("about-accounts-mock-profd");
-    mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-    let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
-    yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
-    info("Wrote file " + fxAccountsStorage);
-
-    // this is a little subtle - we load about:robots so we get a non-remote
-    // tab, then we send a message which does both (a) load the URL we want and
-    // (b) mocks the default profile path used by about:accounts.
-    let tab = yield promiseNewTabLoadEvent("about:robots");
-    let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
-
-    let mm = tab.linkedBrowser.messageManager;
-    mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
-      url: "about:accounts",
-      profilePath: mockDir.path,
-    });
-
-    let response = yield readyPromise;
-    // We are expecting the iframe to be on the "force reauth" URL
-    let expected = yield fxAccounts.promiseAccountsForceSigninURI();
-    is(response.data.url, expected);
-
-    let userData = yield fxAccounts.getSignedInUser();
-    SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
-    // The migration pref will have been switched off by now.
-    is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
-
-    yield OS.File.remove(fxAccountsStorage);
-    yield OS.File.removeEmptyDir(mockDir.path);
-  },
-},
-{
-  desc: "Test with migrateToDevEdition enabled (no user to migrate)",
-  *teardown() {
-    gBrowser.removeCurrentTab();
-    yield signOut();
-  },
-  *run() {
-    const pref = "identity.fxaccounts.migrateToDevEdition";
-    changedPrefs.add(pref);
-    Services.prefs.setBoolPref(pref, true);
-
-    let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
-    let mockDir = profD.clone();
-    mockDir.append("about-accounts-mock-profd");
-    mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-    // but leave it empty, so we don't think a user is logged in.
-
-    let tab = yield promiseNewTabLoadEvent("about:robots");
-    let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
-
-    let mm = tab.linkedBrowser.messageManager;
-    mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
-      url: "about:accounts",
-      profilePath: mockDir.path,
-    });
-
-    let response = yield readyPromise;
-    // We are expecting the iframe to be on the "signup" URL
-    let expected = yield fxAccounts.promiseAccountsSignUpURI();
-    is(response.data.url, expected);
-
-    // and expect no signed in user.
-    let userData = yield fxAccounts.getSignedInUser();
-    is(userData, null);
-    // The migration pref should have still been switched off.
-    is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
-    yield OS.File.removeEmptyDir(mockDir.path);
-  },
-},
-{
-  desc: "Test observers about:accounts",
-  teardown() {
-    gBrowser.removeCurrentTab();
-  },
-  *run() {
-    setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
-    yield setSignedInUser();
-    let tab = yield promiseNewTabLoadEvent("about:accounts");
-    // sign the user out - the tab should have action=signin
-    let loadPromise = promiseOneMessage(tab, "test:document:load");
-    yield signOut();
-    // wait for the new load.
-    yield loadPromise;
-    is(tab.linkedBrowser.contentDocument.location.href, "about:accounts?action=signin");
-  }
-},
-{
-  desc: "Test entrypoint query string, no action, no user logged in",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    // When this loads with no user logged-in, we expect the "normal" URL
-    setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
-    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome");
-    is(url, "https://example.com/?entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
-  },
-},
-{
-  desc: "Test entrypoint query string for signin",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    // When this loads with no user logged-in, we expect the "normal" URL
-    const expected_url = "https://example.com/?is_sign_in";
-    setPref("identity.fxaccounts.remote.signin.uri", expected_url);
-    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin&entrypoint=abouthome");
-    is(url, expected_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
-  },
-},
-{
-  desc: "Test entrypoint query string for signup",
-  teardown: () => gBrowser.removeCurrentTab(),
-  *run() {
-    // When this loads with no user logged-in, we expect the "normal" URL
-    const sign_up_url = "https://example.com/?is_sign_up";
-    setPref("identity.fxaccounts.remote.signup.uri", sign_up_url);
-    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome&action=signup");
-    is(url, sign_up_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
-  },
-},
-{
-  desc: "about:accounts URL params should be copied to remote URL params " +
-        "when remote URL has no URL params, except for 'action'",
-  teardown() {
-    gBrowser.removeCurrentTab();
-  },
-  *run() {
-    let signupURL = "https://example.com/";
-    setPref("identity.fxaccounts.remote.signup.uri", signupURL);
-    let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
-    let [, url] =
-      yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
-                                             "&action=action");
-    is(url, signupURL + "?" + queryStr, "URL params are copied to signup URL");
-  },
-},
-{
-  desc: "about:accounts URL params should be copied to remote URL params " +
-        "when remote URL already has some URL params, except for 'action'",
-  teardown() {
-    gBrowser.removeCurrentTab();
-  },
-  *run() {
-    let signupURL = "https://example.com/?param";
-    setPref("identity.fxaccounts.remote.signup.uri", signupURL);
-    let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
-    let [, url] =
-      yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
-                                             "&action=action");
-    is(url, signupURL + "&" + queryStr, "URL params are copied to signup URL");
-  },
-},
-]; // gTests
-
-function test() {
-  waitForExplicitFinish();
-
-  Task.spawn(function* () {
-    for (let testCase of gTests) {
-      info(testCase.desc);
-      try {
-        yield testCase.run();
-      } finally {
-        yield testCase.teardown();
-      }
-    }
-
-    finish();
-  });
-}
-
-function promiseOneMessage(tab, messageName) {
-  let mm = tab.linkedBrowser.messageManager;
-  let deferred = Promise.defer();
-  mm.addMessageListener(messageName, function onmessage(message) {
-    mm.removeMessageListener(messageName, onmessage);
-    deferred.resolve(message);
-  });
-  return deferred.promise;
-}
-
-function promiseNewTabLoadEvent(aUrl) {
-  let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
-  let browser = tab.linkedBrowser;
-  let mm = browser.messageManager;
-
-  // give it an e10s-friendly content script to help with our tests,
-  // and wait for it to tell us about the load.
-  let promise = promiseOneMessage(tab, "test:document:load");
-  mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
-  return promise.then(() => tab);
-}
-
-// Returns a promise which is resolved with the iframe's URL after a new
-// tab is created and the iframe in that tab loads.
-function promiseNewTabWithIframeLoadEvent(aUrl) {
-  let deferred = Promise.defer();
-  let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
-  let browser = tab.linkedBrowser;
-  let mm = browser.messageManager;
-
-  // give it an e10s-friendly content script to help with our tests,
-  // and wait for it to tell us about the iframe load.
-  mm.addMessageListener("test:iframe:load", function onFrameLoad(message) {
-    mm.removeMessageListener("test:iframe:load", onFrameLoad);
-    deferred.resolve([tab, message.data.url]);
-  });
-  mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
-  return deferred.promise;
-}
-
-function checkVisibilities(tab, data) {
-  let ids = Object.keys(data);
-  let mm = tab.linkedBrowser.messageManager;
-  let deferred = Promise.defer();
-  mm.addMessageListener("test:check-visibilities-response", function onResponse(message) {
-    mm.removeMessageListener("test:check-visibilities-response", onResponse);
-    for (let id of ids) {
-      is(message.data[id], data[id], "Element '" + id + "' has correct visibility");
-    }
-    deferred.resolve();
-  });
-  mm.sendAsyncMessage("test:check-visibilities", {ids});
-  return deferred.promise;
-}
-
-// watch out - these will fire observers which if you aren't careful, may
-// interfere with the tests.
-function setSignedInUser(data) {
-  if (!data) {
-    data = {
-      email: "foo@example.com",
-      uid: "1234@lcip.org",
-      assertion: "foobar",
-      sessionToken: "dead",
-      kA: "beef",
-      kB: "cafe",
-      verified: true
-    }
-  }
- return fxAccounts.setSignedInUser(data);
-}
-
-function signOut() {
-  // we always want a "localOnly" signout here...
-  return fxAccounts.signOut(true);
-}
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -898,17 +898,17 @@ add_task(function* test_input_spell_fals
     ]
   );
   */
 });
 
 const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
 
 add_task(function* test_plaintext_sendpagetodevice() {
-  if (!gFxAccounts.sendTabToDeviceEnabled) {
+  if (!gSync.sendTabToDeviceEnabled) {
     return;
   }
   yield ensureSyncReady();
   const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
 
   let plainTextItemsWithSendPage =
                     ["context-navigation",   null,
                       ["context-back",         false,
@@ -937,17 +937,17 @@ add_task(function* test_plaintext_sendpa
         yield openMenuItemSubmenu("context-sendpagetodevice");
       }
     });
 
   restoreRemoteClients(oldGetter);
 });
 
 add_task(function* test_link_sendlinktodevice() {
-  if (!gFxAccounts.sendTabToDeviceEnabled) {
+  if (!gSync.sendTabToDeviceEnabled) {
     return;
   }
   yield ensureSyncReady();
   const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
 
   yield test_contextmenu("#test-link",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxa_web_channel.js
+++ /dev/null
@@ -1,210 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
-  return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
-});
-
-XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
-                                  "resource://gre/modules/WebChannel.jsm");
-
-// FxAccountsWebChannel isn't explicitly exported by FxAccountsWebChannel.jsm
-// but we can get it here via a backstage pass.
-var {FxAccountsWebChannel} = Components.utils.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});
-
-const TEST_HTTP_PATH = "http://example.com";
-const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/general/browser_fxa_web_channel.html";
-const TEST_CHANNEL_ID = "account_updates_test";
-
-var gTests = [
-  {
-    desc: "FxA Web Channel - should receive message about profile changes",
-    *run() {
-      let client = new FxAccountsWebChannel({
-        content_uri: TEST_HTTP_PATH,
-        channel_id: TEST_CHANNEL_ID,
-      });
-      let promiseObserver = new Promise((resolve, reject) => {
-        makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
-          Assert.equal(data, "abc123");
-          client.tearDown();
-          resolve();
-        });
-      });
-
-      yield BrowserTestUtils.withNewTab({
-        gBrowser,
-        url: TEST_BASE_URL + "?profile_change"
-      }, function* () {
-        yield promiseObserver;
-      });
-    }
-  },
-  {
-    desc: "fxa web channel - login messages should notify the fxAccounts object",
-    *run() {
-
-      let promiseLogin = new Promise((resolve, reject) => {
-        let login = (accountData) => {
-          Assert.equal(typeof accountData.authAt, "number");
-          Assert.equal(accountData.email, "testuser@testuser.com");
-          Assert.equal(accountData.keyFetchToken, "key_fetch_token");
-          Assert.equal(accountData.sessionToken, "session_token");
-          Assert.equal(accountData.uid, "uid");
-          Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
-          Assert.equal(accountData.verified, true);
-
-          client.tearDown();
-          resolve();
-        };
-
-        let client = new FxAccountsWebChannel({
-          content_uri: TEST_HTTP_PATH,
-          channel_id: TEST_CHANNEL_ID,
-          helpers: {
-            login
-          }
-        });
-      });
-
-      yield BrowserTestUtils.withNewTab({
-        gBrowser,
-        url: TEST_BASE_URL + "?login"
-      }, function* () {
-        yield promiseLogin;
-      });
-    }
-  },
-  {
-    desc: "fxa web channel - can_link_account messages should respond",
-    *run() {
-      let properUrl = TEST_BASE_URL + "?can_link_account";
-
-      let promiseEcho = new Promise((resolve, reject) => {
-
-        let webChannelOrigin = Services.io.newURI(properUrl);
-        // responses sent to content are echoed back over the
-        // `fxaccounts_webchannel_response_echo` channel. Ensure the
-        // fxaccounts:can_link_account message is responded to.
-        let echoWebChannel = new WebChannel("fxaccounts_webchannel_response_echo", webChannelOrigin);
-        echoWebChannel.listen((webChannelId, message, target) => {
-          Assert.equal(message.command, "fxaccounts:can_link_account");
-          Assert.equal(message.messageId, 2);
-          Assert.equal(message.data.ok, true);
-
-          client.tearDown();
-          echoWebChannel.stopListening();
-
-          resolve();
-        });
-
-        let client = new FxAccountsWebChannel({
-          content_uri: TEST_HTTP_PATH,
-          channel_id: TEST_CHANNEL_ID,
-          helpers: {
-            shouldAllowRelink(acctName) {
-              return acctName === "testuser@testuser.com";
-            }
-          }
-        });
-      });
-
-      yield BrowserTestUtils.withNewTab({
-        gBrowser,
-        url: properUrl
-      }, function* () {
-        yield promiseEcho;
-      });
-    }
-  },
-  {
-    desc: "fxa web channel - logout messages should notify the fxAccounts object",
-    *run() {
-      let promiseLogout = new Promise((resolve, reject) => {
-        let logout = (uid) => {
-          Assert.equal(uid, "uid");
-
-          client.tearDown();
-          resolve();
-        };
-
-        let client = new FxAccountsWebChannel({
-          content_uri: TEST_HTTP_PATH,
-          channel_id: TEST_CHANNEL_ID,
-          helpers: {
-            logout
-          }
-        });
-      });
-
-      yield BrowserTestUtils.withNewTab({
-        gBrowser,
-        url: TEST_BASE_URL + "?logout"
-      }, function* () {
-        yield promiseLogout;
-      });
-    }
-  },
-  {
-    desc: "fxa web channel - delete messages should notify the fxAccounts object",
-    *run() {
-      let promiseDelete = new Promise((resolve, reject) => {
-        let logout = (uid) => {
-          Assert.equal(uid, "uid");
-
-          client.tearDown();
-          resolve();
-        };
-
-        let client = new FxAccountsWebChannel({
-          content_uri: TEST_HTTP_PATH,
-          channel_id: TEST_CHANNEL_ID,
-          helpers: {
-            logout
-          }
-        });
-      });
-
-      yield BrowserTestUtils.withNewTab({
-        gBrowser,
-        url: TEST_BASE_URL + "?delete"
-      }, function* () {
-        yield promiseDelete;
-      });
-    }
-  }
-]; // gTests
-
-function makeObserver(aObserveTopic, aObserveFunc) {
-  let callback = function(aSubject, aTopic, aData) {
-    if (aTopic == aObserveTopic) {
-      removeMe();
-      aObserveFunc(aSubject, aTopic, aData);
-    }
-  };
-
-  function removeMe() {
-    Services.obs.removeObserver(callback, aObserveTopic);
-  }
-
-  Services.obs.addObserver(callback, aObserveTopic);
-  return removeMe;
-}
-
-function test() {
-  waitForExplicitFinish();
-
-  Task.spawn(function* () {
-    for (let testCase of gTests) {
-      info("Running: " + testCase.desc);
-      yield testCase.run();
-    }
-  }).then(finish, ex => {
-    Assert.ok(false, "Unexpected Exception: " + ex);
-    finish();
-  });
-}
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxaccounts.js
+++ /dev/null
@@ -1,258 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
-var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
-var {fxAccounts} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
-var FxAccountsCommon = {};
-Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
-
-const TEST_ROOT = "http://example.com/browser/browser/base/content/test/general/";
-
-// instrument gFxAccounts to send observer notifications when it's done
-// what it does.
-(function() {
-  let unstubs = {}; // The original functions we stub out.
-
-  // The stub functions.
-  let stubs = {
-    updateUI() {
-      return unstubs["updateUI"].call(gFxAccounts).then(() => {
-        Services.obs.notifyObservers(null, "test:browser_fxaccounts:updateUI");
-      });
-    },
-    // Opening preferences is trickier than it should be as leaks are reported
-    // due to the promises it fires off at load time  and there's no clear way to
-    // know when they are done.
-    // So just ensure openPreferences is called rather than whether it opens.
-    openPreferences() {
-      Services.obs.notifyObservers(null, "test:browser_fxaccounts:openPreferences");
-    }
-  };
-
-  for (let name in stubs) {
-    unstubs[name] = gFxAccounts[name];
-    gFxAccounts[name] = stubs[name];
-  }
-  // and undo our damage at the end.
-  registerCleanupFunction(() => {
-    for (let name in unstubs) {
-      gFxAccounts[name] = unstubs[name];
-    }
-    stubs = unstubs = null;
-  });
-})();
-
-// Other setup/cleanup
-var newTab;
-
-Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri",
-                           TEST_ROOT + "accounts_testRemoteCommands.html");
-
-registerCleanupFunction(() => {
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
-  Services.prefs.clearUserPref("identity.fxaccounts.remote.profile.uri");
-  gBrowser.removeTab(newTab);
-});
-
-add_task(function* initialize() {
-  // Set a new tab with something other than about:blank, so it doesn't get reused.
-  // We must wait for it to load or the promiseTabOpen() call in the next test
-  // gets confused.
-  newTab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
-  yield promiseTabLoaded(newTab);
-});
-
-// The elements we care about.
-var panelUILabel = document.getElementById("PanelUI-fxa-label");
-var panelUIStatus = document.getElementById("PanelUI-fxa-status");
-var panelUIFooter = document.getElementById("PanelUI-footer-fxa");
-
-// The tests
-add_task(function* test_nouser() {
-  let user = yield fxAccounts.getSignedInUser();
-  Assert.strictEqual(user, null, "start with no user signed in");
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
-  Services.obs.notifyObservers(null, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION);
-  yield promiseUpdateDone;
-
-  // Check the world - the FxA footer area is visible as it is offering a signin.
-  Assert.ok(isFooterVisible())
-
-  Assert.equal(panelUILabel.getAttribute("label"), panelUIStatus.getAttribute("defaultlabel"));
-  Assert.equal(panelUIStatus.getAttribute("tooltiptext"), panelUIStatus.getAttribute("signedinTooltiptext"));
-  Assert.ok(!panelUIFooter.hasAttribute("fxastatus"), "no fxsstatus when signed out");
-  Assert.ok(!panelUIFooter.hasAttribute("fxaprofileimage"), "no fxaprofileimage when signed out");
-
-  let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
-  panelUIStatus.click();
-  yield promisePreferencesOpened;
-});
-
-/*
-XXX - Bug 1191162 - need a better hawk mock story or this will leak in debug builds.
-
-add_task(function* test_unverifiedUser() {
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
-  yield setSignedInUser(false); // this will fire the observer that does the update.
-  yield promiseUpdateDone;
-
-  // Check the world.
-  Assert.ok(isFooterVisible())
-
-  Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
-  Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
-               panelUIStatus.getAttribute("signedinTooltiptext"));
-  Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
-  let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
-  panelUIStatus.click();
-  yield promisePreferencesOpened
-  yield signOut();
-});
-*/
-
-add_task(function* test_verifiedUserEmptyProfile() {
-  // We see 2 updateUI() calls - one for the signedInUser and one after
-  // we first fetch the profile. We want them both to fire or we aren't testing
-  // the state we think we are testing.
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 2);
-  gFxAccounts._cachedProfile = null;
-  configureProfileURL({}); // successful but empty profile.
-  yield setSignedInUser(true); // this will fire the observer that does the update.
-  yield promiseUpdateDone;
-
-  // Check the world.
-  Assert.ok(isFooterVisible())
-  Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
-  Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
-               panelUIStatus.getAttribute("signedinTooltiptext"));
-  Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
-
-  let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
-  panelUIStatus.click();
-  yield promisePreferencesOpened;
-  yield signOut();
-});
-
-add_task(function* test_verifiedUserDisplayName() {
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 2);
-  gFxAccounts._cachedProfile = null;
-  configureProfileURL({ displayName: "Test User Display Name" });
-  yield setSignedInUser(true); // this will fire the observer that does the update.
-  yield promiseUpdateDone;
-
-  Assert.ok(isFooterVisible())
-  Assert.equal(panelUILabel.getAttribute("label"), "Test User Display Name");
-  Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
-               panelUIStatus.getAttribute("signedinTooltiptext"));
-  Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
-  yield signOut();
-});
-
-add_task(function* test_profileNotificationsClearsCache() {
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 1);
-  gFxAccounts._cachedProfile = { foo: "bar" };
-  Services.obs.notifyObservers(null, this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION);
-  Assert.ok(!gFxAccounts._cachedProfile);
-  yield promiseUpdateDone;
-});
-
-add_task(function* test_verifiedUserProfileFailure() {
-  // profile failure means only one observer fires.
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 1);
-  gFxAccounts._cachedProfile = null;
-  configureProfileURL(null, 500);
-  yield setSignedInUser(true); // this will fire the observer that does the update.
-  yield promiseUpdateDone;
-
-  Assert.ok(isFooterVisible());
-  Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
-  Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
-               panelUIStatus.getAttribute("signedinTooltiptext"));
-  Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
-  yield signOut();
-});
-
-// Helpers.
-function isFooterVisible() {
-  let style = window.getComputedStyle(panelUIFooter);
-  return style.getPropertyValue("display") == "flex";
-}
-
-function configureProfileURL(profile, responseStatus = 200) {
-  let responseBody = profile ? JSON.stringify(profile) : "";
-  let url = TEST_ROOT + "fxa_profile_handler.sjs?" +
-            "responseStatus=" + responseStatus +
-            "responseBody=" + responseBody +
-            // This is a bit cheeky - the FxA code will just append "/profile"
-            // to the preference value. We arrange for this to be seen by our
-            // .sjs as part of the query string.
-            "&path=";
-
-  Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", url);
-}
-
-function promiseObserver(topic, count = 1) {
-  return new Promise(resolve => {
-    let obs = (aSubject, aTopic, aData) => {
-      if (--count == 0) {
-        Services.obs.removeObserver(obs, aTopic);
-        resolve(aSubject);
-      }
-    }
-    Services.obs.addObserver(obs, topic);
-  });
-}
-
-var promiseTabOpen = Task.async(function*(urlBase) {
-  info("Waiting for tab to open...");
-  let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
-  let tab = event.target;
-  yield promiseTabLoadEvent(tab);
-  ok(tab.linkedBrowser.currentURI.spec.startsWith(urlBase),
-     "Got " + tab.linkedBrowser.currentURI.spec + ", expecting " + urlBase);
-  let whenUnloaded = promiseTabUnloaded(tab);
-  gBrowser.removeTab(tab);
-  yield whenUnloaded;
-});
-
-function promiseTabUnloaded(tab) {
-  return new Promise(resolve => {
-    info("Wait for tab to unload");
-    function handle(event) {
-      tab.linkedBrowser.removeEventListener("unload", handle, true);
-      info("Got unload event");
-      resolve(event);
-    }
-    tab.linkedBrowser.addEventListener("unload", handle, true, true);
-  });
-}
-
-// FxAccounts helpers.
-function setSignedInUser(verified) {
-  let data = {
-    email: "foo@example.com",
-    uid: "1234@lcip.org",
-    assertion: "foobar",
-    sessionToken: "dead",
-    kA: "beef",
-    kB: "cafe",
-    verified,
-
-    oauthTokens: {
-      // a token for the profile server.
-      profile: "key value",
-    }
-  }
-  return fxAccounts.setSignedInUser(data);
-}
-
-var signOut = Task.async(function* () {
-  // This test needs to make sure that any updates for the logout have
-  // completed before starting the next test, or we see the observer
-  // notifications get out of sync.
-  let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
-  // we always want a "localOnly" signout here...
-  yield fxAccounts.signOut(true);
-  yield promiseUpdateDone;
-});
deleted file mode 100644
--- a/browser/base/content/test/general/browser_syncui.js
+++ /dev/null
@@ -1,205 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
-var {Weave} = Cu.import("resource://services-sync/main.js", {});
-
-var stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
-                   .getService(Ci.nsIStringBundleService)
-                   .createBundle("chrome://weave/locale/sync.properties");
-
-// ensure test output sees log messages.
-Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender());
-
-// Send the specified sync-related notification and return a promise that
-// resolves once gSyncUI._promiseUpateUI is complete and the UI is ready to check.
-function notifyAndPromiseUIUpdated(topic) {
-  return new Promise(resolve => {
-    // Instrument gSyncUI so we know when the update is complete.
-    let oldPromiseUpdateUI = gSyncUI._promiseUpdateUI.bind(gSyncUI);
-    gSyncUI._promiseUpdateUI = function() {
-      return oldPromiseUpdateUI().then(() => {
-        // Restore our override.
-        gSyncUI._promiseUpdateUI = oldPromiseUpdateUI;
-        // Resolve the promise so the caller knows the update is done.
-        resolve();
-      });
-    };
-    // Now send the notification.
-    Services.obs.notifyObservers(null, topic);
-  });
-}
-
-// Sync manages 3 broadcasters so the menus correctly reflect the Sync state.
-// Only one of these 3 should ever be visible - pass the ID of the broadcaster
-// you expect to be visible and it will check it's the only one that is.
-function checkBroadcasterVisible(broadcasterId) {
-  let all = ["sync-reauth-state", "sync-setup-state", "sync-syncnow-state"];
-  Assert.ok(all.indexOf(broadcasterId) >= 0, "valid id");
-  for (let check of all) {
-    let eltHidden = document.getElementById(check).hidden;
-    Assert.equal(eltHidden, check == broadcasterId ? false : true, check);
-  }
-}
-
-function promiseObserver(topic) {
-  return new Promise(resolve => {
-    let obs = (aSubject, aTopic, aData) => {
-      Services.obs.removeObserver(obs, aTopic);
-      resolve(aSubject);
-    }
-    Services.obs.addObserver(obs, topic);
-  });
-}
-
-function checkButtonTooltips(stringPrefix) {
-  for (let butId of ["PanelUI-remotetabs-syncnow", "PanelUI-fxa-icon"]) {
-    let text = document.getElementById(butId).getAttribute("tooltiptext");
-    let desc = `Text is "${text}", expecting it to start with "${stringPrefix}"`
-    Assert.ok(text.startsWith(stringPrefix), desc);
-  }
-}
-
-add_task(function* prepare() {
-  // add the Sync button to the toolbar so we can get it!
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_NAVBAR);
-  registerCleanupFunction(() => {
-    CustomizableUI.removeWidgetFromArea("sync-button");
-  });
-
-  let xps = Components.classes["@mozilla.org/weave/service;1"]
-                              .getService(Components.interfaces.nsISupports)
-                              .wrappedJSObject;
-  yield xps.whenLoaded();
-  // Put Sync and the UI into a known state.
-  Weave.Status.login = Weave.LOGIN_FAILED_NO_USERNAME;
-  yield notifyAndPromiseUIUpdated("weave:service:login:error");
-
-  checkBroadcasterVisible("sync-setup-state");
-  checkButtonTooltips("Sign In To Sync");
-  // mock out the "_needsSetup()" function so we don't short-circuit.
-  let oldNeedsSetup = window.gSyncUI._needsSetup;
-  window.gSyncUI._needsSetup = () => Promise.resolve(false);
-  registerCleanupFunction(() => {
-    window.gSyncUI._needsSetup = oldNeedsSetup;
-    // and an observer to set the state back to what it should be now we've
-    // restored the stub.
-    Services.obs.notifyObservers(null, "weave:service:login:finish");
-  });
-  // and a notification to have the state change away from "needs setup"
-  yield notifyAndPromiseUIUpdated("weave:service:login:finish");
-  checkBroadcasterVisible("sync-syncnow-state");
-  // open the sync-button panel so we can check elements in that.
-  document.getElementById("sync-button").click();
-});
-
-add_task(function* testSyncNeedsVerification() {
-  // mock out the "_needsVerification()" function
-  let oldNeedsVerification = window.gSyncUI._needsVerification;
-  window.gSyncUI._needsVerification = () => true;
-  try {
-    // a notification for the state change
-    yield notifyAndPromiseUIUpdated("weave:service:login:finish");
-    checkButtonTooltips("Verify");
-  } finally {
-    window.gSyncUI._needsVerification = oldNeedsVerification;
-  }
-});
-
-
-add_task(function* testSyncLoginError() {
-  checkBroadcasterVisible("sync-syncnow-state");
-
-  // Pretend we are in a "login failed" error state
-  Weave.Status.sync = Weave.LOGIN_FAILED;
-  Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
-  yield notifyAndPromiseUIUpdated("weave:ui:sync:error");
-
-  // But the menu *should* reflect the login error.
-  checkBroadcasterVisible("sync-reauth-state");
-  // The tooltips for the buttons should also reflect it.
-  checkButtonTooltips("Reconnect");
-
-  // Now pretend we just had a successful login - the error notification should go away.
-  Weave.Status.sync = Weave.STATUS_OK;
-  Weave.Status.login = Weave.LOGIN_SUCCEEDED;
-  yield notifyAndPromiseUIUpdated("weave:service:login:start");
-  yield notifyAndPromiseUIUpdated("weave:service:login:finish");
-  // The menus should be back to "all good"
-  checkBroadcasterVisible("sync-syncnow-state");
-});
-
-function checkButtonsStatus(shouldBeActive) {
-  for (let eid of [
-    "sync-status", // the broadcaster itself.
-    "sync-button", // the main sync button which observes the broadcaster
-    "PanelUI-fxa-icon", // the sync icon in the fxa footer that observes it.
-    ]) {
-    let elt = document.getElementById(eid);
-    if (shouldBeActive) {
-      Assert.equal(elt.getAttribute("syncstatus"), "active", `${eid} should be active`);
-    } else {
-      Assert.ok(!elt.hasAttribute("syncstatus"), `${eid} should have no status attr`);
-    }
-  }
-}
-
-function* testButtonActions(startNotification, endNotification, expectActive = true) {
-  checkButtonsStatus(false);
-  // pretend a sync is starting.
-  yield notifyAndPromiseUIUpdated(startNotification);
-  checkButtonsStatus(expectActive);
-  // and has stopped
-  yield notifyAndPromiseUIUpdated(endNotification);
-  checkButtonsStatus(false);
-}
-
-function *doTestButtonActivities() {
-  // logins do not "activate" the spinner/button as they may block and make
-  // the UI look like Sync is never completing.
-  yield testButtonActions("weave:service:login:start", "weave:service:login:finish", false);
-  yield testButtonActions("weave:service:login:start", "weave:service:login:error", false);
-
-  // But notifications for Sync itself should activate it.
-  yield testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
-  yield testButtonActions("weave:service:sync:start", "weave:service:sync:error");
-
-  // and ensure the counters correctly handle multiple in-flight syncs
-  yield notifyAndPromiseUIUpdated("weave:service:sync:start");
-  checkButtonsStatus(true);
-  // sync stops.
-  yield notifyAndPromiseUIUpdated("weave:service:sync:finish");
-  // Button should not be active.
-  checkButtonsStatus(false);
-}
-
-add_task(function* testButtonActivitiesInNavBar() {
-  // check the button's functionality while the button is in the NavBar - which
-  // it already is.
-  yield doTestButtonActivities();
-});
-
-add_task(function* testFormatLastSyncDateNow() {
-  let now = new Date();
-  let nowString = gSyncUI.formatLastSyncDate(now);
-  Assert.equal(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
-});
-
-add_task(function* testFormatLastSyncDateMonthAgo() {
-  let monthAgo = new Date();
-  monthAgo.setMonth(monthAgo.getMonth() - 1);
-  let monthAgoString = gSyncUI.formatLastSyncDate(monthAgo);
-  Assert.equal(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}));
-});
-
-add_task(function* testButtonActivitiesInPanel() {
-  // check the button's functionality while the button is in the panel - it's
-  // currently in the NavBar - move it to the panel and open it.
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
-  yield PanelUI.show();
-  try {
-    yield doTestButtonActivities();
-  } finally {
-    PanelUI.hide();
-  }
-});
--- a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -12,35 +12,35 @@ add_task(function* test() {
   is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
 
   // Check the context menu with two tabs
   updateTabContextMenu(origTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
   is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
 
 
-  if (gFxAccounts.sendTabToDeviceEnabled) {
-    const origIsSendableURI = gFxAccounts.isSendableURI;
-    gFxAccounts.isSendableURI = () => true;
+  if (gSync.sendTabToDeviceEnabled) {
+    const origIsSendableURI = gSync.isSendableURI;
+    gSync.isSendableURI = () => true;
     // Check the send tab to device menu item
     yield ensureSyncReady();
     const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
     yield updateTabContextMenu(origTab, function* () {
       yield openMenuItemSubmenu("context_sendTabToDevice");
     });
     is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
     let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
     is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
     is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
     is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
-    gFxAccounts.isSendableURI = () => false;
+    gSync.isSendableURI = () => false;
     updateTabContextMenu(origTab);
     is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
     restoreRemoteClients(oldGetter);
-    gFxAccounts.isSendableURI = origIsSendableURI;
+    gSync.isSendableURI = origIsSendableURI;
   }
 
   // Hide the original tab.
   gBrowser.selectedTab = testTab;
   gBrowser.showOnlyTheseTabs([testTab]);
   is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
 
   // Check the context menu with one tab.
deleted file mode 100644
--- a/browser/base/content/test/general/fxa_profile_handler.sjs
+++ /dev/null
@@ -1,34 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// This is basically an echo server!
-// We just grab responseStatus and responseBody query params!
-
-function reallyHandleRequest(request, response) {
-  var query = "?" + request.queryString;
-
-  var responseStatus = 200;
-  var match = /responseStatus=([^&]*)/.exec(query);
-  if (match) {
-    responseStatus = parseInt(match[1]);
-  }
-
-  var responseBody = "";
-  match = /responseBody=([^&]*)/.exec(query);
-  if (match) {
-    responseBody = decodeURIComponent(match[1]);
-  }
-
-  response.setStatusLine("1.0", responseStatus, "OK");
-  response.write(responseBody);
-}
-
-function handleRequest(request, response)
-{
-  try {
-    reallyHandleRequest(request, response);
-  } catch (e) {
-    response.setStatusLine("1.0", 500, "NotOK");
-    response.write("Error handling request: " + e);
-  }
-}
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -831,26 +831,26 @@ function getCertExceptionDialog(aLocatio
       }
     }
   }
   return undefined;
 }
 
 function setupRemoteClientsFixture(fixture) {
   let oldRemoteClientsGetter =
-    Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
+    Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
 
-  Object.defineProperty(gFxAccounts, "remoteClients", {
+  Object.defineProperty(gSync, "remoteClients", {
     get() { return fixture; }
   });
   return oldRemoteClientsGetter;
 }
 
 function restoreRemoteClients(getter) {
-  Object.defineProperty(gFxAccounts, "remoteClients", {
+  Object.defineProperty(gSync, "remoteClients", {
     get: getter
   });
 }
 
 function* openMenuItemSubmenu(id) {
   let menuPopup = document.getElementById(id).menupopup;
   let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
   menuPopup.showPopup();
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
+};
rename from browser/base/content/test/general/accounts_testRemoteCommands.html
rename to browser/base/content/test/sync/accounts_testRemoteCommands.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser.ini
@@ -0,0 +1,9 @@
+[browser_sync.js]
+[browser_fxa_web_channel.js]
+support-files=
+  browser_fxa_web_channel.html
+[browser_aboutAccounts.js]
+skip-if = os == "linux" # Bug 958026
+support-files =
+  content_aboutAccounts.js
+  accounts_testRemoteCommands.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_aboutAccounts.js
@@ -0,0 +1,487 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+  "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+
+const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/sync/";
+// Preference helpers.
+var changedPrefs = new Set();
+
+function setPref(name, value) {
+  changedPrefs.add(name);
+  Services.prefs.setCharPref(name, value);
+}
+
+registerCleanupFunction(function() {
+  // Ensure we don't pollute prefs for next tests.
+  for (let name of changedPrefs) {
+    Services.prefs.clearUserPref(name);
+  }
+});
+
+var gTests = [
+{
+  desc: "Test the remote commands",
+  *teardown() {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  *run() {
+    setPref("identity.fxaccounts.remote.signup.uri",
+            "https://example.com/browser/browser/base/content/test/sync/accounts_testRemoteCommands.html");
+    let tab = yield promiseNewTabLoadEvent("about:accounts");
+    let mm = tab.linkedBrowser.messageManager;
+
+    let deferred = Promise.defer();
+
+    // We'll get a message when openPrefs() is called, which this test should
+    // arrange.
+    let promisePrefsOpened = promiseOneMessage(tab, "test:openPrefsCalled");
+    let results = 0;
+    try {
+      mm.addMessageListener("test:response", function responseHandler(msg) {
+        let data = msg.data.data;
+        if (data.type == "testResult") {
+          ok(data.pass, data.info);
+          results++;
+        } else if (data.type == "testsComplete") {
+          is(results, data.count, "Checking number of results received matches the number of tests that should have run");
+          mm.removeMessageListener("test:response", responseHandler);
+          deferred.resolve();
+        }
+      });
+    } catch (e) {
+      ok(false, "Failed to get all commands");
+      deferred.reject();
+    }
+    yield deferred.promise;
+    yield promisePrefsOpened;
+  }
+},
+{
+  desc: "Test action=signin - no user logged in",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    // When this loads with no user logged-in, we expect the "normal" URL
+    const expected_url = "https://example.com/?is_sign_in";
+    setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+    let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+    is(url, expected_url, "action=signin got the expected URL");
+    // we expect the remote iframe to be shown.
+    yield checkVisibilities(tab, {
+      stage: false, // parent of 'manage' and 'intro'
+      manage: false,
+      intro: false, // this is  "get started"
+      remote: true,
+      networkError: false
+    });
+  }
+},
+{
+  desc: "Test action=signin - user logged in",
+  *teardown() {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  *run() {
+    // When this loads with a user logged-in, we expect the normal URL to
+    // have been ignored and the "manage" page to be shown.
+    const expected_url = "https://example.com/?is_sign_in";
+    setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+    yield setSignedInUser();
+    let tab = yield promiseNewTabLoadEvent("about:accounts?action=signin");
+    // about:accounts initializes after fetching the current user from Fxa -
+    // so we also request it - by the time we get it we know it should have
+    // done its thing.
+    yield fxAccounts.getSignedInUser();
+    // we expect "manage" to be shown.
+    yield checkVisibilities(tab, {
+      stage: true, // parent of 'manage' and 'intro'
+      manage: true,
+      intro: false, // this is  "get started"
+      remote: false,
+      networkError: false
+    });
+  }
+},
+{
+  desc: "Test action=signin - captive portal",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    const signinUrl = "https://redirproxy.example.com/test";
+    setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
+    let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+    yield checkVisibilities(tab, {
+      stage: true, // parent of 'manage' and 'intro'
+      manage: false,
+      intro: false, // this is  "get started"
+      remote: false,
+      networkError: true
+    });
+  }
+},
+{
+  desc: "Test action=signin - offline",
+  teardown: () => {
+    gBrowser.removeCurrentTab();
+    BrowserOffline.toggleOfflineStatus();
+  },
+  *run() {
+    BrowserOffline.toggleOfflineStatus();
+    Services.cache2.clear();
+
+    const signinUrl = "https://unknowndomain.cow";
+    setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
+    let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+    yield checkVisibilities(tab, {
+      stage: true, // parent of 'manage' and 'intro'
+      manage: false,
+      intro: false, // this is  "get started"
+      remote: false,
+      networkError: true
+    });
+  }
+},
+{
+  desc: "Test action=signup - no user logged in",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    const expected_url = "https://example.com/?is_sign_up";
+    setPref("identity.fxaccounts.remote.signup.uri", expected_url);
+    let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signup");
+    is(url, expected_url, "action=signup got the expected URL");
+    // we expect the remote iframe to be shown.
+    yield checkVisibilities(tab, {
+      stage: false, // parent of 'manage' and 'intro'
+      manage: false,
+      intro: false, // this is  "get started"
+      remote: true,
+      networkError: false
+    });
+  },
+},
+{
+  desc: "Test action=signup - user logged in",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    const expected_url = "https://example.com/?is_sign_up";
+    setPref("identity.fxaccounts.remote.signup.uri", expected_url);
+    yield setSignedInUser();
+    let tab = yield promiseNewTabLoadEvent("about:accounts?action=signup");
+    yield fxAccounts.getSignedInUser();
+    // we expect "manage" to be shown.
+    yield checkVisibilities(tab, {
+      stage: true, // parent of 'manage' and 'intro'
+      manage: true,
+      intro: false, // this is  "get started"
+      remote: false,
+      networkError: false
+    });
+  },
+},
+{
+  desc: "Test action=reauth",
+  *teardown() {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  *run() {
+    const expected_url = "https://example.com/force_auth";
+    setPref("identity.fxaccounts.remote.force_auth.uri", expected_url);
+
+    yield setSignedInUser();
+    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
+    // The current user will be appended to the url
+    let expected = expected_url + "?uid=1234%40lcip.org&email=foo%40example.com";
+    is(url, expected, "action=reauth got the expected URL");
+  },
+},
+{
+  desc: "Test with migrateToDevEdition enabled (success)",
+  *teardown() {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  *run() {
+    let fxAccountsCommon = {};
+    Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+    const pref = "identity.fxaccounts.migrateToDevEdition";
+    changedPrefs.add(pref);
+    Services.prefs.setBoolPref(pref, true);
+
+    // Create the signedInUser.json file that will be used as the source of
+    // migrated user data.
+    let signedInUser = {
+      version: 1,
+      accountData: {
+        email: "foo@example.com",
+        uid: "1234@lcip.org",
+        sessionToken: "dead",
+        verified: true
+      }
+    };
+    // We use a sub-dir of the real profile dir as the "pretend" profile dir
+    // for this test.
+    let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    let mockDir = profD.clone();
+    mockDir.append("about-accounts-mock-profd");
+    mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+    yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
+    info("Wrote file " + fxAccountsStorage);
+
+    // this is a little subtle - we load about:robots so we get a non-remote
+    // tab, then we send a message which does both (a) load the URL we want and
+    // (b) mocks the default profile path used by about:accounts.
+    let tab = yield promiseNewTabLoadEvent("about:robots");
+    let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+    let mm = tab.linkedBrowser.messageManager;
+    mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+      url: "about:accounts",
+      profilePath: mockDir.path,
+    });
+
+    let response = yield readyPromise;
+    // We are expecting the iframe to be on the "force reauth" URL
+    let expected = yield fxAccounts.promiseAccountsForceSigninURI();
+    is(response.data.url, expected);
+
+    let userData = yield fxAccounts.getSignedInUser();
+    SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
+    // The migration pref will have been switched off by now.
+    is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+
+    yield OS.File.remove(fxAccountsStorage);
+    yield OS.File.removeEmptyDir(mockDir.path);
+  },
+},
+{
+  desc: "Test with migrateToDevEdition enabled (no user to migrate)",
+  *teardown() {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  *run() {
+    const pref = "identity.fxaccounts.migrateToDevEdition";
+    changedPrefs.add(pref);
+    Services.prefs.setBoolPref(pref, true);
+
+    let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    let mockDir = profD.clone();
+    mockDir.append("about-accounts-mock-profd");
+    mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    // but leave it empty, so we don't think a user is logged in.
+
+    let tab = yield promiseNewTabLoadEvent("about:robots");
+    let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+    let mm = tab.linkedBrowser.messageManager;
+    mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+      url: "about:accounts",
+      profilePath: mockDir.path,
+    });
+
+    let response = yield readyPromise;
+    // We are expecting the iframe to be on the "signup" URL
+    let expected = yield fxAccounts.promiseAccountsSignUpURI();
+    is(response.data.url, expected);
+
+    // and expect no signed in user.
+    let userData = yield fxAccounts.getSignedInUser();
+    is(userData, null);
+    // The migration pref should have still been switched off.
+    is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+    yield OS.File.removeEmptyDir(mockDir.path);
+  },
+},
+{
+  desc: "Test observers about:accounts",
+  teardown() {
+    gBrowser.removeCurrentTab();
+  },
+  *run() {
+    setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
+    yield setSignedInUser();
+    let tab = yield promiseNewTabLoadEvent("about:accounts");
+    // sign the user out - the tab should have action=signin
+    let loadPromise = promiseOneMessage(tab, "test:document:load");
+    yield signOut();
+    // wait for the new load.
+    yield loadPromise;
+    is(tab.linkedBrowser.contentDocument.location.href, "about:accounts?action=signin");
+  }
+},
+{
+  desc: "Test entrypoint query string, no action, no user logged in",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    // When this loads with no user logged-in, we expect the "normal" URL
+    setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
+    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome");
+    is(url, "https://example.com/?entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+  },
+},
+{
+  desc: "Test entrypoint query string for signin",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    // When this loads with no user logged-in, we expect the "normal" URL
+    const expected_url = "https://example.com/?is_sign_in";
+    setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin&entrypoint=abouthome");
+    is(url, expected_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+  },
+},
+{
+  desc: "Test entrypoint query string for signup",
+  teardown: () => gBrowser.removeCurrentTab(),
+  *run() {
+    // When this loads with no user logged-in, we expect the "normal" URL
+    const sign_up_url = "https://example.com/?is_sign_up";
+    setPref("identity.fxaccounts.remote.signup.uri", sign_up_url);
+    let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome&action=signup");
+    is(url, sign_up_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+  },
+},
+{
+  desc: "about:accounts URL params should be copied to remote URL params " +
+        "when remote URL has no URL params, except for 'action'",
+  teardown() {
+    gBrowser.removeCurrentTab();
+  },
+  *run() {
+    let signupURL = "https://example.com/";
+    setPref("identity.fxaccounts.remote.signup.uri", signupURL);
+    let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
+    let [, url] =
+      yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
+                                             "&action=action");
+    is(url, signupURL + "?" + queryStr, "URL params are copied to signup URL");
+  },
+},
+{
+  desc: "about:accounts URL params should be copied to remote URL params " +
+        "when remote URL already has some URL params, except for 'action'",
+  teardown() {
+    gBrowser.removeCurrentTab();
+  },
+  *run() {
+    let signupURL = "https://example.com/?param";
+    setPref("identity.fxaccounts.remote.signup.uri", signupURL);
+    let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
+    let [, url] =
+      yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
+                                             "&action=action");
+    is(url, signupURL + "&" + queryStr, "URL params are copied to signup URL");
+  },
+},
+]; // gTests
+
+function test() {
+  waitForExplicitFinish();
+
+  Task.spawn(function* () {
+    for (let testCase of gTests) {
+      info(testCase.desc);
+      try {
+        yield testCase.run();
+      } finally {
+        yield testCase.teardown();
+      }
+    }
+
+    finish();
+  });
+}
+
+function promiseOneMessage(tab, messageName) {
+  let mm = tab.linkedBrowser.messageManager;
+  let deferred = Promise.defer();
+  mm.addMessageListener(messageName, function onmessage(message) {
+    mm.removeMessageListener(messageName, onmessage);
+    deferred.resolve(message);
+  });
+  return deferred.promise;
+}
+
+function promiseNewTabLoadEvent(aUrl) {
+  let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+  let browser = tab.linkedBrowser;
+  let mm = browser.messageManager;
+
+  // give it an e10s-friendly content script to help with our tests,
+  // and wait for it to tell us about the load.
+  let promise = promiseOneMessage(tab, "test:document:load");
+  mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
+  return promise.then(() => tab);
+}
+
+// Returns a promise which is resolved with the iframe's URL after a new
+// tab is created and the iframe in that tab loads.
+function promiseNewTabWithIframeLoadEvent(aUrl) {
+  let deferred = Promise.defer();
+  let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+  let browser = tab.linkedBrowser;
+  let mm = browser.messageManager;
+
+  // give it an e10s-friendly content script to help with our tests,
+  // and wait for it to tell us about the iframe load.
+  mm.addMessageListener("test:iframe:load", function onFrameLoad(message) {
+    mm.removeMessageListener("test:iframe:load", onFrameLoad);
+    deferred.resolve([tab, message.data.url]);
+  });
+  mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
+  return deferred.promise;
+}
+
+function checkVisibilities(tab, data) {
+  let ids = Object.keys(data);
+  let mm = tab.linkedBrowser.messageManager;
+  let deferred = Promise.defer();
+  mm.addMessageListener("test:check-visibilities-response", function onResponse(message) {
+    mm.removeMessageListener("test:check-visibilities-response", onResponse);
+    for (let id of ids) {
+      is(message.data[id], data[id], "Element '" + id + "' has correct visibility");
+    }
+    deferred.resolve();
+  });
+  mm.sendAsyncMessage("test:check-visibilities", {ids});
+  return deferred.promise;
+}
+
+// watch out - these will fire observers which if you aren't careful, may
+// interfere with the tests.
+function setSignedInUser(data) {
+  if (!data) {
+    data = {
+      email: "foo@example.com",
+      uid: "1234@lcip.org",
+      assertion: "foobar",
+      sessionToken: "dead",
+      kA: "beef",
+      kB: "cafe",
+      verified: true
+    }
+  }
+ return fxAccounts.setSignedInUser(data);
+}
+
+function signOut() {
+  // we always want a "localOnly" signout here...
+  return fxAccounts.signOut(true);
+}
rename from browser/base/content/test/general/browser_fxa_web_channel.html
rename to browser/base/content/test/sync/browser_fxa_web_channel.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
+  return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+                                  "resource://gre/modules/WebChannel.jsm");
+
+// FxAccountsWebChannel isn't explicitly exported by FxAccountsWebChannel.jsm
+// but we can get it here via a backstage pass.
+var {FxAccountsWebChannel} = Components.utils.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});
+
+const TEST_HTTP_PATH = "http://example.com";
+const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/sync/browser_fxa_web_channel.html";
+const TEST_CHANNEL_ID = "account_updates_test";
+
+var gTests = [
+  {
+    desc: "FxA Web Channel - should receive message about profile changes",
+    *run() {
+      let client = new FxAccountsWebChannel({
+        content_uri: TEST_HTTP_PATH,
+        channel_id: TEST_CHANNEL_ID,
+      });
+      let promiseObserver = new Promise((resolve, reject) => {
+        makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
+          Assert.equal(data, "abc123");
+          client.tearDown();
+          resolve();
+        });
+      });
+
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: TEST_BASE_URL + "?profile_change"
+      }, function* () {
+        yield promiseObserver;
+      });
+    }
+  },
+  {
+    desc: "fxa web channel - login messages should notify the fxAccounts object",
+    *run() {
+
+      let promiseLogin = new Promise((resolve, reject) => {
+        let login = (accountData) => {
+          Assert.equal(typeof accountData.authAt, "number");
+          Assert.equal(accountData.email, "testuser@testuser.com");
+          Assert.equal(accountData.keyFetchToken, "key_fetch_token");
+          Assert.equal(accountData.sessionToken, "session_token");
+          Assert.equal(accountData.uid, "uid");
+          Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
+          Assert.equal(accountData.verified, true);
+
+          client.tearDown();
+          resolve();
+        };
+
+        let client = new FxAccountsWebChannel({
+          content_uri: TEST_HTTP_PATH,
+          channel_id: TEST_CHANNEL_ID,
+          helpers: {
+            login
+          }
+        });
+      });
+
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: TEST_BASE_URL + "?login"
+      }, function* () {
+        yield promiseLogin;
+      });
+    }
+  },
+  {
+    desc: "fxa web channel - can_link_account messages should respond",
+    *run() {
+      let properUrl = TEST_BASE_URL + "?can_link_account";
+
+      let promiseEcho = new Promise((resolve, reject) => {
+
+        let webChannelOrigin = Services.io.newURI(properUrl);
+        // responses sent to content are echoed back over the
+        // `fxaccounts_webchannel_response_echo` channel. Ensure the
+        // fxaccounts:can_link_account message is responded to.
+        let echoWebChannel = new WebChannel("fxaccounts_webchannel_response_echo", webChannelOrigin);
+        echoWebChannel.listen((webChannelId, message, target) => {
+          Assert.equal(message.command, "fxaccounts:can_link_account");
+          Assert.equal(message.messageId, 2);
+          Assert.equal(message.data.ok, true);
+
+          client.tearDown();
+          echoWebChannel.stopListening();
+
+          resolve();
+        });
+
+        let client = new FxAccountsWebChannel({
+          content_uri: TEST_HTTP_PATH,
+          channel_id: TEST_CHANNEL_ID,
+          helpers: {
+            shouldAllowRelink(acctName) {
+              return acctName === "testuser@testuser.com";
+            }
+          }
+        });
+      });
+
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: properUrl
+      }, function* () {
+        yield promiseEcho;
+      });
+    }
+  },
+  {
+    desc: "fxa web channel - logout messages should notify the fxAccounts object",
+    *run() {
+      let promiseLogout = new Promise((resolve, reject) => {
+        let logout = (uid) => {
+          Assert.equal(uid, "uid");
+
+          client.tearDown();
+          resolve();
+        };
+
+        let client = new FxAccountsWebChannel({
+          content_uri: TEST_HTTP_PATH,
+          channel_id: TEST_CHANNEL_ID,
+          helpers: {
+            logout
+          }
+        });
+      });
+
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: TEST_BASE_URL + "?logout"
+      }, function* () {
+        yield promiseLogout;
+      });
+    }
+  },
+  {
+    desc: "fxa web channel - delete messages should notify the fxAccounts object",
+    *run() {
+      let promiseDelete = new Promise((resolve, reject) => {
+        let logout = (uid) => {
+          Assert.equal(uid, "uid");
+
+          client.tearDown();
+          resolve();
+        };
+
+        let client = new FxAccountsWebChannel({
+          content_uri: TEST_HTTP_PATH,
+          channel_id: TEST_CHANNEL_ID,
+          helpers: {
+            logout
+          }
+        });
+      });
+
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: TEST_BASE_URL + "?delete"
+      }, function* () {
+        yield promiseDelete;
+      });
+    }
+  }
+]; // gTests
+
+function makeObserver(aObserveTopic, aObserveFunc) {
+  let callback = function(aSubject, aTopic, aData) {
+    if (aTopic == aObserveTopic) {
+      removeMe();
+      aObserveFunc(aSubject, aTopic, aData);
+    }
+  };
+
+  function removeMe() {
+    Services.obs.removeObserver(callback, aObserveTopic);
+  }
+
+  Services.obs.addObserver(callback, aObserveTopic);
+  return removeMe;
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  Task.spawn(function* () {
+    for (let testCase of gTests) {
+      info("Running: " + testCase.desc);
+      yield testCase.run();
+    }
+  }).then(finish, ex => {
+    Assert.ok(false, "Unexpected Exception: " + ex);
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -0,0 +1,241 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_ui_state_notification_calls_updateAllUI() {
+  let called = false;
+  let updateAllUI = gSync.updateAllUI;
+  gSync.updateAllUI = () => { called = true; };
+
+  Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+  ok(called);
+
+  gSync.updateAllUI = updateAllUI;
+});
+
+add_task(async function test_ui_state_signedin() {
+  let state = {
+    status: UIState.STATUS_SIGNED_IN,
+    email: "foo@bar.com",
+    displayName: "Foo Bar",
+    avatarURL: "https://foo.bar",
+    lastSync: new Date(),
+    syncing: false
+  };
+
+  gSync.updateAllUI(state);
+
+  checkFxABadge(false);
+  let statusBarTooltip = gSync.panelUIStatus.getAttribute("signedinTooltiptext");
+  let lastSyncTooltip = gSync.formatLastSyncDate(new Date(state.lastSync));
+  checkPanelUIStatusBar({
+    label: "Foo Bar",
+    tooltip: statusBarTooltip,
+    fxastatus: "signedin",
+    avatarURL: "https://foo.bar",
+    syncing: false,
+    syncNowTooltip: lastSyncTooltip
+  });
+  checkRemoteTabsPanel("PanelUI-remotetabs-main", false);
+  checkMenuBarItem("sync-syncnowitem");
+});
+
+add_task(async function test_ui_state_syncing() {
+  let state = {
+    status: UIState.STATUS_SIGNED_IN,
+    email: "foo@bar.com",
+    displayName: "Foo Bar",
+    avatarURL: "https://foo.bar",
+    lastSync: new Date(),
+    syncing: true
+  };
+
+  gSync.updateAllUI(state);
+
+  checkSyncNowButton("PanelUI-fxa-icon", true);
+  checkSyncNowButton("PanelUI-remotetabs-syncnow", true);
+
+  // Be good citizens and remove the "syncing" state.
+  gSync.updateAllUI({
+    status: UIState.STATUS_SIGNED_IN,
+    email: "foo@bar.com",
+    lastSync: new Date(),
+    syncing: false
+  });
+  // Because we switch from syncing to non-syncing, and there's a timeout involved.
+  await promiseObserver("test:browser-sync:activity-stop");
+});
+
+add_task(async function test_ui_state_unconfigured() {
+  let state = {
+    status: UIState.STATUS_NOT_CONFIGURED
+  };
+
+  gSync.updateAllUI(state);
+
+  checkFxABadge(false);
+  let signedOffLabel = gSync.panelUIStatus.getAttribute("defaultlabel");
+  let statusBarTooltip = gSync.panelUIStatus.getAttribute("signedinTooltiptext");
+  checkPanelUIStatusBar({
+    label: signedOffLabel,
+    tooltip: statusBarTooltip
+  });
+  checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
+  checkMenuBarItem("sync-setup");
+});
+
+add_task(async function test_ui_state_unverified() {
+  let state = {
+    status: UIState.STATUS_NOT_VERIFIED,
+    email: "foo@bar.com",
+    lastSync: new Date(),
+    syncing: false
+  };
+
+  gSync.updateAllUI(state);
+
+  checkFxABadge(true);
+  let expectedLabel = gSync.panelUIStatus.getAttribute("unverifiedlabel");
+  let tooltipText = gSync.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
+  checkPanelUIStatusBar({
+    label: expectedLabel,
+    tooltip: tooltipText,
+    fxastatus: "unverified",
+    avatarURL: null,
+    syncing: false,
+    syncNowTooltip: tooltipText
+  });
+  checkRemoteTabsPanel("PanelUI-remotetabs-setupsync", false);
+  checkMenuBarItem("sync-setup");
+});
+
+add_task(async function test_ui_state_loginFailed() {
+  let state = {
+    status: UIState.STATUS_LOGIN_FAILED,
+    email: "foo@bar.com"
+  };
+
+  gSync.updateAllUI(state);
+
+  checkFxABadge(true);
+  let expectedLabel = gSync.panelUIStatus.getAttribute("errorlabel");
+  let tooltipText = gSync.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
+  checkPanelUIStatusBar({
+    label: expectedLabel,
+    tooltip: tooltipText,
+    fxastatus: "login-failed",
+    avatarURL: null,
+    syncing: false,
+    syncNowTooltip: tooltipText
+  });
+  checkRemoteTabsPanel("PanelUI-remotetabs-reauthsync", false);
+  checkMenuBarItem("sync-reauthitem");
+});
+
+add_task(async function test_FormatLastSyncDateNow() {
+  let now = new Date();
+  let nowString = gSync.formatLastSyncDate(now);
+  is(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}),
+     "The date is correctly formatted");
+});
+
+add_task(async function test_FormatLastSyncDateMonthAgo() {
+  let monthAgo = new Date();
+  monthAgo.setMonth(monthAgo.getMonth() - 1);
+  let monthAgoString = gSync.formatLastSyncDate(monthAgo);
+  is(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}),
+     "The date is correctly formatted");
+});
+
+function checkFxABadge(shouldBeShown) {
+  let isShown = false;
+  for (let notification of PanelUI.notifications) {
+    if (notification.id == "fxa-needs-authentication") {
+      isShown = true;
+      break;
+    }
+  }
+  is(isShown, shouldBeShown, "the fxa badge has the right visibility");
+}
+
+function checkPanelUIStatusBar({label, tooltip, fxastatus, avatarURL, syncing, syncNowTooltip}) {
+  let labelNode = document.getElementById("PanelUI-fxa-label");
+  let tooltipNode = document.getElementById("PanelUI-fxa-status");
+  let statusNode = document.getElementById("PanelUI-footer-fxa");
+  let avatar = document.getElementById("PanelUI-fxa-avatar");
+
+  is(labelNode.getAttribute("label"), label, "panelUI-fxa label has the right value");
+  is(tooltipNode.getAttribute("tooltiptext"), tooltip, "panelUI-fxa tooltip has the right value");
+  if (fxastatus) {
+    is(statusNode.getAttribute("fxastatus"), fxastatus, "panelUI-fxa fxastatus has the right value");
+  } else {
+    ok(!statusNode.hasAttribute("fxastatus"), "panelUI-fxa fxastatus is unset")
+  }
+  if (avatarURL) {
+    is(avatar.style.listStyleImage, `url("${avatarURL}")`, "panelUI-fxa avatar URL is set");
+  } else {
+    ok(!statusNode.style.listStyleImage, "panelUI-fxa avatar URL is unset");
+  }
+
+  if (syncing != undefined && syncNowTooltip != undefined) {
+    checkSyncNowButton("PanelUI-fxa-icon", syncing, syncNowTooltip);
+  }
+}
+
+function checkRemoteTabsPanel(expectedShownItemId, syncing, syncNowTooltip) {
+  checkItemsVisiblities(["PanelUI-remotetabs-main",
+                         "PanelUI-remotetabs-setupsync",
+                         "PanelUI-remotetabs-reauthsync"],
+                        expectedShownItemId);
+
+  if (syncing != undefined && syncNowTooltip != undefined) {
+    checkSyncNowButton("PanelUI-remotetabs-syncnow", syncing, syncNowTooltip);
+  }
+}
+
+function checkMenuBarItem(expectedShownItemId) {
+  checkItemsVisiblities(["sync-setup", "sync-syncnowitem", "sync-reauthitem"],
+                        expectedShownItemId);
+}
+
+function checkSyncNowButton(buttonId, syncing, tooltip = null) {
+  const remoteTabsButton = document.getElementById(buttonId);
+
+  is(remoteTabsButton.getAttribute("syncstatus"), syncing ? "active" : "", "button active has the right value");
+  if (tooltip) {
+    is(remoteTabsButton.getAttribute("tooltiptext"), tooltip, "button tooltiptext is set to the right value");
+  }
+
+  if (buttonId == "PanelUI-fxa-icon") {
+    return;
+  }
+
+  is(remoteTabsButton.hasAttribute("disabled"), syncing, "disabled has the right value");
+  if (syncing) {
+    is(remoteTabsButton.getAttribute("label"), gSync.syncStrings.GetStringFromName("syncing2.label"), "label is set to the right value");
+  } else {
+    is(remoteTabsButton.getAttribute("label"), gSync.syncStrings.GetStringFromName("syncnow.label"), "label is set to the right value");
+  }
+}
+
+// Only one item visible at a time.
+function checkItemsVisiblities(itemsIds, expectedShownItemId) {
+  for (let id of itemsIds) {
+    if (id == expectedShownItemId) {
+      ok(!document.getElementById(id).hidden, "menuitem " + id + " should be visible");
+    } else {
+      ok(document.getElementById(id).hidden, "menuitem " + id + " should be hidden");
+    }
+  }
+}
+
+function promiseObserver(topic) {
+  return new Promise(resolve => {
+    let obs = (aSubject, aTopic, aData) => {
+      Services.obs.removeObserver(obs, aTopic);
+      resolve(aSubject);
+    }
+    Services.obs.addObserver(obs, topic);
+  });
+}
rename from browser/base/content/test/general/content_aboutAccounts.js
rename to browser/base/content/test/sync/content_aboutAccounts.js
--- a/browser/base/content/web-panels.xul
+++ b/browser/base/content/web-panels.xul
@@ -19,17 +19,17 @@
 <page id="webpanels-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="load()" onunload="unload()">
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
-  <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
+  <script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
   <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
   <script type="application/javascript" src="chrome://browser/content/web-panels.js"/>
 
   <stringbundleset id="stringbundleset"> 
     <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
   </stringbundleset>
 
   <broadcasterset id="mainBroadcasterSet">
--- a/browser/base/content/webext-panels.xul
+++ b/browser/base/content/webext-panels.xul
@@ -19,17 +19,17 @@
 <page id="webextpanels-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="load()">
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
-  <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
+  <script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
   <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
   <script type="application/javascript" src="chrome://browser/content/webext-panels.js"/>
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
   </stringbundleset>
 
   <broadcasterset id="mainBroadcasterSet">
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -68,26 +68,25 @@ browser.jar:
         content/browser/browser-captivePortal.js      (content/browser-captivePortal.js)
         content/browser/browser-ctrlTab.js            (content/browser-ctrlTab.js)
         content/browser/browser-customization.js      (content/browser-customization.js)
         content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
         content/browser/browser-compacttheme.js       (content/browser-compacttheme.js)
         content/browser/browser-feeds.js              (content/browser-feeds.js)
         content/browser/browser-fullScreenAndPointerLock.js  (content/browser-fullScreenAndPointerLock.js)
         content/browser/browser-fullZoom.js           (content/browser-fullZoom.js)
-        content/browser/browser-fxaccounts.js         (content/browser-fxaccounts.js)
         content/browser/browser-gestureSupport.js     (content/browser-gestureSupport.js)
         content/browser/browser-media.js              (content/browser-media.js)
         content/browser/browser-places.js             (content/browser-places.js)
         content/browser/browser-plugins.js            (content/browser-plugins.js)
         content/browser/browser-refreshblocker.js     (content/browser-refreshblocker.js)
         content/browser/browser-safebrowsing.js       (content/browser-safebrowsing.js)
         content/browser/browser-sidebar.js            (content/browser-sidebar.js)
         content/browser/browser-social.js             (content/browser-social.js)
-        content/browser/browser-syncui.js             (content/browser-syncui.js)
+        content/browser/browser-sync.js               (content/browser-sync.js)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 #ifdef CAN_DRAW_IN_TITLEBAR
         content/browser/browser-tabsintitlebar.js       (content/browser-tabsintitlebar.js)
 #else
         content/browser/browser-tabsintitlebar.js       (content/browser-tabsintitlebar-stub.js)
 #endif
         content/browser/browser-thumbnails.js         (content/browser-thumbnails.js)
         content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -24,16 +24,17 @@ BROWSER_CHROME_MANIFESTS += [
     'content/test/permissions/browser.ini',
     'content/test/plugins/browser.ini',
     'content/test/popupNotifications/browser.ini',
     'content/test/popups/browser.ini',
     'content/test/referrer/browser.ini',
     'content/test/siteIdentity/browser.ini',
     'content/test/social/browser.ini',
     'content/test/static/browser.ini',
+    'content/test/sync/browser.ini',
     'content/test/tabcrashed/browser.ini',
     'content/test/tabPrompts/browser.ini',
     'content/test/tabs/browser.ini',
     'content/test/urlbar/browser.ini',
     'content/test/webextensions/browser.ini',
     'content/test/webrtc/browser.ini',
     'content/test/windows/browser.ini',
 ]
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -467,17 +467,17 @@ const CustomizableWidgets = [
     },
     _appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
       let doc = attachFragment.ownerDocument;
       // Create the element for the remote client.
       let clientItem = doc.createElementNS(kNSXUL, "label");
       clientItem.setAttribute("itemtype", "client");
       let window = doc.defaultView;
       clientItem.setAttribute("tooltiptext",
-        window.gSyncUI.formatLastSyncDate(new Date(client.lastModified)));
+        window.gSync.formatLastSyncDate(new Date(client.lastModified)));
       clientItem.textContent = client.name;
 
       attachFragment.appendChild(clientItem);
 
       if (client.tabs.length == 0) {
         let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
         label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
       } else {
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -26,29 +26,31 @@
                        label="&updateManual.panelUI.label;"
                        hidden="true"/>
         <toolbarbutton id="PanelUI-update-restart-menu-item"
                        wrap="true"
                        label="&updateRestart.panelUI.label;"
                        hidden="true"/>
         <hbox id="PanelUI-footer-fxa">
           <hbox id="PanelUI-fxa-status"
+                label="&fxaSignedIn.tooltip;"
                 defaultlabel="&fxaSignIn.label;"
                 signedinTooltiptext="&fxaSignedIn.tooltip;"
                 tooltiptext="&fxaSignedIn.tooltip;"
                 errorlabel="&fxaSignInError.label;"
                 unverifiedlabel="&fxaUnverified.label;"
-                onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();">
+                onclick="if (event.which == 1) gSync.onMenuPanelCommand();">
             <image id="PanelUI-fxa-avatar"/>
             <toolbarbutton id="PanelUI-fxa-label"
+                           label="&fxaSignIn.label;"
                            fxabrandname="&syncBrand.fxAccount.label;"/>
           </hbox>
           <toolbarseparator/>
           <toolbarbutton id="PanelUI-fxa-icon"
-                         oncommand="gSyncUI.doSync();"
+                         oncommand="gSync.doSync();"
                          closemenu="none">
             <observes element="sync-status" attribute="syncstatus"/>
             <observes element="sync-status" attribute="tooltiptext"/>
           </toolbarbutton>
         </hbox>
 
         <hbox id="PanelUI-footer-inner">
           <toolbarbutton id="PanelUI-customize" label="&appMenuCustomize.label;"
@@ -120,21 +122,21 @@
           <vbox id="PanelUI-remotetabs-buttons">
             <toolbarbutton id="PanelUI-remotetabs-view-sidebar"
                            class="subviewbutton"
                            observes="viewTabsSidebar"
                            label="&appMenuRemoteTabs.sidebar.label;"/>
             <toolbarbutton id="PanelUI-remotetabs-view-managedevices"
                            class="subviewbutton"
                            label="&appMenuRemoteTabs.managedevices.label;"
-                           oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-menupanel');"/>
+                           oncommand="gSync.openDevicesManagementPage('syncedtabs-menupanel');"/>
             <toolbarbutton id="PanelUI-remotetabs-syncnow"
                            observes="sync-status"
                            class="subviewbutton"
-                           oncommand="gSyncUI.doSync();"
+                           oncommand="gSync.doSync();"
                            closemenu="none"/>
             <menuseparator id="PanelUI-remotetabs-separator"/>
           </vbox>
           <deck id="PanelUI-remotetabs-deck">
             <!-- Sync is ready to Sync and the "tabs" engine is enabled -->
             <vbox id="PanelUI-remotetabs-tabspane">
               <vbox id="PanelUI-remotetabs-tabslist"
                     showAllLabel="&appMenuRemoteTabs.showAll.label;"
@@ -149,17 +151,17 @@
               <vbox class="PanelUI-remotetabs-instruction-box" align="center">
                 <hbox pack="center">
                   <html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
                 </hbox>
                 <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label>
                 <hbox pack="center">
                   <toolbarbutton class="PanelUI-remotetabs-prefs-button"
                                  label="&appMenuRemoteTabs.openprefs.label;"
-                                 oncommand="gSyncUI.openPrefs('synced-tabs');"/>
+                                 oncommand="gSync.openPrefs('synced-tabs');"/>
                 </hbox>
               </vbox>
             </hbox>
             <!-- Sync is ready to Sync but we are still fetching the tabs to show -->
             <vbox id="PanelUI-remotetabs-fetching">
               <!-- Show intentionally blank panel, see bug 1239845 -->
             </vbox>
             <!-- Sync has only 1 (ie, this) device connected -->
@@ -183,31 +185,31 @@
                 flex="1"
                 align="center"
                 class="PanelUI-remotetabs-instruction-box"
                 observes="sync-setup-state">
             <html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
             <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
             <toolbarbutton class="PanelUI-remotetabs-prefs-button"
                            label="&appMenuRemoteTabs.signin.label;"
-                           oncommand="gSyncUI.openPrefs('synced-tabs');"/>
+                           oncommand="gSync.openPrefs('synced-tabs');"/>
           </vbox>
           <!-- When Sync needs re-authentication. This uses the exact same messaging
                as "Sync is not configured" but remains a separate box so we get
                the goodness of observing broadcasters to manage the hidden states -->
           <vbox id="PanelUI-remotetabs-reauthsync"
                 flex="1"
                 align="center"
                 class="PanelUI-remotetabs-instruction-box"
                 observes="sync-reauth-state">
             <html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
             <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
             <toolbarbutton class="PanelUI-remotetabs-prefs-button"
                            label="&appMenuRemoteTabs.signin.label;"
-                           oncommand="gSyncUI.openPrefs('synced-tabs');"/>
+                           oncommand="gSync.openPrefs('synced-tabs');"/>
           </vbox>
         </hbox>
       </vbox>
     </panelview>
 
     <panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView">
       <label value="&bookmarksMenu.label;" class="panel-subview-header"/>
       <vbox class="panel-subview-body">
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -117,17 +117,16 @@ skip-if = os == "linux"
 [browser_981305_separator_insertion.js]
 [browser_981418-widget-onbeforecreated-handler.js]
 [browser_982656_restore_defaults_builtin_widgets.js]
 [browser_984455_bookmarks_items_reparenting.js]
 skip-if = os == "linux"
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_987177_destroyWidget_xul.js]
 [browser_987177_xul_wrapper_updating.js]
-[browser_987185_syncButton.js]
 [browser_987492_window_api.js]
 [browser_987640_charEncoding.js]
 [browser_988072_sidebar_events.js]
 [browser_989338_saved_placements_not_resaved.js]
 [browser_989751_subviewbutton_class.js]
 [browser_992747_toggle_noncustomizable_toolbar.js]
 [browser_993322_widget_notoolbar.js]
 [browser_995164_registerArea_during_customize_mode.js]
@@ -152,8 +151,9 @@ skip-if = os == "mac"
 [browser_overflow_use_subviews.js]
 [browser_panel_toggle.js]
 [browser_panelUINotifications.js]
 [browser_switch_to_customize_mode.js]
 [browser_synced_tabs_menu.js]
 [browser_check_tooltips_in_navbar.js]
 [browser_editcontrols_update.js]
 subsuite = clipboard
+[browser_remote_tabs_button.js]
deleted file mode 100755
--- a/browser/components/customizableui/test/browser_987185_syncButton.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-"use strict";
-
-var syncService = {};
-Components.utils.import("resource://services-sync/service.js", syncService);
-
-var needsSetup;
-var originalSync;
-var service = syncService.Service;
-var syncWasCalled = false;
-
-add_task(function* testSyncButtonFunctionality() {
-  info("Check Sync button functionality");
-  storeInitialValues();
-  mockFunctions();
-
-  // add the Sync button to the panel
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
-
-  // check the button's functionality
-  yield PanelUI.show();
-  info("The panel menu was opened");
-
-  let syncButton = document.getElementById("sync-button");
-  ok(syncButton, "The Sync button was added to the Panel Menu");
-  // click the button - the panel should open.
-  syncButton.click();
-  let syncPanel = document.getElementById("PanelUI-remotetabs");
-  ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
-
-  // Find and click the "setup" button.
-  let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
-  syncNowButton.click();
-
-  info("The sync button was clicked");
-
-  yield waitForCondition(() => syncWasCalled);
-});
-
-add_task(function* asyncCleanup() {
-  // reset the panel UI to the default state
-  yield resetCustomization();
-  ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
-
-  if (isPanelUIOpen()) {
-    let panelHidePromise = promisePanelHidden(window);
-    PanelUI.hide();
-    yield panelHidePromise;
-  }
-
-  restoreValues();
-});
-
-function mockFunctions() {
-  // mock needsSetup
-  gSyncUI._needsSetup = () => Promise.resolve(false);
-
-  // mock service.errorHandler.syncAndReportErrors()
-  service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
-}
-
-function mocked_syncAndReportErrors() {
-  syncWasCalled = true;
-}
-
-function restoreValues() {
-  gSyncUI._needsSetup = needsSetup;
-  service.sync = originalSync;
-}
-
-function storeInitialValues() {
-  needsSetup = gSyncUI._needsSetup;
-  originalSync = service.sync;
-}
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_remote_tabs_button.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+"use strict";
+
+let syncService = {};
+Components.utils.import("resource://services-sync/service.js", syncService);
+const service = syncService.Service;
+Components.utils.import("resource://services-sync/UIState.jsm");
+
+let getState;
+let originalSync;
+let syncWasCalled = false;
+
+// TODO: This test should probably be re-written, we don't really test much here.
+add_task(async function testSyncRemoteTabsButtonFunctionality() {
+  info("Test the Sync Remote Tabs button in the PanelUI");
+  storeInitialValues();
+  mockFunctions();
+
+  // Force UI update.
+  Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+  // add the sync remote tabs button to the panel
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+
+  // check the button's functionality
+  await PanelUI.show();
+  info("The panel menu was opened");
+
+  let syncRemoteTabsBtn = document.getElementById("sync-button");
+  ok(syncRemoteTabsBtn, "The sync remote tabs button was added to the Panel Menu");
+  // click the button - the panel should open.
+  syncRemoteTabsBtn.click();
+  let remoteTabsPanel = document.getElementById("PanelUI-remotetabs");
+  ok(remoteTabsPanel.getAttribute("current"), "Sync Panel is in view");
+
+  // Find and click the "setup" button.
+  let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
+  syncNowButton.click();
+  info("The sync now button was clicked");
+
+  await waitForCondition(() => syncWasCalled);
+});
+
+add_task(async function asyncCleanup() {
+  // reset the panel UI to the default state
+  await resetCustomization();
+  ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
+
+  if (isPanelUIOpen()) {
+    let panelHidePromise = promisePanelHidden(window);
+    PanelUI.hide();
+    await panelHidePromise;
+  }
+
+  restoreValues();
+});
+
+function mockFunctions() {
+  // mock UIState.get()
+  UIState.get = () => ({
+    status: UIState.STATUS_SIGNED_IN,
+    email: "user@mozilla.com"
+  });
+
+  // mock service.errorHandler.syncAndReportErrors()
+  service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
+}
+
+function mocked_syncAndReportErrors() {
+  syncWasCalled = true;
+}
+
+function restoreValues() {
+  UIState.get = getState;
+  service.syncAndReportErrors = originalSync;
+}
+
+function storeInitialValues() {
+  getState = UIState.get;
+  originalSync = service.syncAndReportErrors;
+}
--- a/browser/components/customizableui/test/browser_synced_tabs_menu.js
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -224,21 +224,21 @@ add_task(function* () {
 
   // The widget is still fetching tabs, as we've neutered everything that
   // provides them
   is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
 
   let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
 
   let didSync = false;
-  let oldDoSync = gSyncUI.doSync;
-  gSyncUI.doSync = function() {
+  let oldDoSync = gSync.doSync;
+  gSync.doSync = function() {
     didSync = true;
     mockedInternal.hasSyncedThisSession = true;
-    gSyncUI.doSync = oldDoSync;
+    gSync.doSync = oldDoSync;
   }
   syncNowButton.click();
   ok(didSync, "clicking the button called the correct function");
 
   // Tell the widget there are tabs available, but with zero clients.
   mockedInternal.getTabClients = () => {
     return Promise.resolve([]);
   }
--- a/browser/components/syncedtabs/SyncedTabsDeckComponent.js
+++ b/browser/components/syncedtabs/SyncedTabsDeckComponent.js
@@ -117,17 +117,17 @@ SyncedTabsDeckComponent.prototype = {
   // There's no good way to mock fxAccounts in browser tests where it's already
   // been instantiated, so we have this method for stubbing.
   _accountStatus() {
     return this._fxAccounts.accountStatus();
   },
 
   getPanelStatus() {
     return this._accountStatus().then(exists => {
-      if (!exists || this._getChromeWindow(this._window).gSyncUI.loginFailed()) {
+      if (!exists || this._SyncedTabs.loginFailed) {
         return this.PANELS.NOT_AUTHED_INFO;
       }
       if (!this._SyncedTabs.isConfiguredToSyncTabs) {
         return this.PANELS.TABS_DISABLED;
       }
       if (!this._SyncedTabs.hasSyncedThisSession) {
         return this.PANELS.TABS_FETCHING;
       }
@@ -161,12 +161,12 @@ SyncedTabsDeckComponent.prototype = {
     this._openUrl(href, event);
   },
 
   _openUrl(url, event) {
     this._window.openUILink(url, event);
   },
 
   openSyncPrefs() {
-    this._getChromeWindow(this._window).gSyncUI.openPrefs("tabs-sidebar");
+    this._getChromeWindow(this._window).gSync.openPrefs("tabs-sidebar");
   }
 };
 
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -208,17 +208,17 @@ TabListView.prototype = {
    * Update the element representing an item, ensuring it's in sync with the
    * underlying data.
    * @param {client} item - Item to use as a source.
    * @param {Element} itemNode - Element to update.
    */
   _updateClient(item, itemNode) {
     itemNode.setAttribute("id", "item-" + item.id);
     let lastSync = new Date(item.lastModified);
-    let lastSyncTitle = getChromeWindow(this._window).gSyncUI.formatLastSyncDate(lastSync);
+    let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(lastSync);
     itemNode.setAttribute("title", lastSyncTitle);
     if (item.closed) {
       itemNode.classList.add("closed");
     } else {
       itemNode.classList.remove("closed");
     }
     if (item.selected) {
       itemNode.classList.add("selected");
--- a/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
+++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
@@ -135,49 +135,38 @@ add_task(function* testPanelStatus() {
   let listStore = new SyncedTabsListStore();
   let listComponent = {};
   let fxAccounts = {
     accountStatus() {}
   };
   let SyncedTabsMock = {
     getTabClients() {}
   };
-  let loginFailed = false;
-  let chromeWindowMock = {
-    gSyncUI: {
-      loginFailed() {
-        return loginFailed;
-      }
-    }
-  };
-  let getChromeWindowMock = sinon.stub();
-  getChromeWindowMock.returns(chromeWindowMock);
 
   sinon.stub(listStore, "getData");
 
 
   let component = new SyncedTabsDeckComponent({
     fxAccounts,
     deckStore,
     listComponent,
-    SyncedTabs: SyncedTabsMock,
-    getChromeWindowMock
+    SyncedTabs: SyncedTabsMock
   });
 
   let isAuthed = false;
   sinon.stub(fxAccounts, "accountStatus", () => Promise.resolve(isAuthed));
   let result = yield component.getPanelStatus();
   Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
 
   isAuthed = true;
 
-  loginFailed = true;
+  SyncedTabsMock.loginFailed = true;
   result = yield component.getPanelStatus();
   Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
-  loginFailed = false;
+  SyncedTabsMock.loginFailed = false;
 
   SyncedTabsMock.isConfiguredToSyncTabs = false;
   result = yield component.getPanelStatus();
   Assert.equal(result, component.PANELS.TABS_DISABLED);
 
   SyncedTabsMock.isConfiguredToSyncTabs = true;
 
   SyncedTabsMock.hasSyncedThisSession = false;
@@ -206,22 +195,22 @@ add_task(function* testPanelStatus() {
   Assert.ok(deckStore.selectPanel.calledWith("mock-panelId"));
 });
 
 add_task(function* testActions() {
   let windowMock = {
     openUILink() {},
   };
   let chromeWindowMock = {
-    gSyncUI: {
+    gSync: {
       openPrefs() {}
     }
   };
   sinon.spy(windowMock, "openUILink");
-  sinon.spy(chromeWindowMock.gSyncUI, "openPrefs");
+  sinon.spy(chromeWindowMock.gSync, "openPrefs");
 
   let getChromeWindowMock = sinon.stub();
   getChromeWindowMock.returns(chromeWindowMock);
 
   let component = new SyncedTabsDeckComponent({
     window: windowMock,
     getChromeWindowMock
   });
@@ -231,10 +220,10 @@ add_task(function* testActions() {
   Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
 
   href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
   component.openiOSLink("mock-event");
   Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
 
   component.openSyncPrefs();
   Assert.ok(getChromeWindowMock.calledWith(windowMock));
-  Assert.ok(chromeWindowMock.gSyncUI.openPrefs.called);
+  Assert.ok(chromeWindowMock.gSync.openPrefs.called);
 });
--- a/browser/components/uitour/test/browser_fxa.js
+++ b/browser/components/uitour/test/browser_fxa.js
@@ -13,34 +13,34 @@ var gContentAPI;
 var gContentWindow;
 
 function test() {
   UITourTest();
 }
 
 registerCleanupFunction(function*() {
   yield signOut();
-  gFxAccounts.updateUI();
+  gSync.updateAllUI(UIState.get());
 });
 
 var tests = [
   taskify(function* test_highlight_accountStatus_loggedOut() {
     let userData = yield fxAccounts.getSignedInUser();
     is(userData, null, "Not logged in initially");
     yield showMenuPromise("appMenu");
     yield showHighlightPromise("accountStatus");
     let highlight = document.getElementById("UITourHighlightContainer");
     is(highlight.getAttribute("targetName"), "accountStatus", "Correct highlight target");
   }),
 
   taskify(function* test_highlight_accountStatus_loggedIn() {
     yield setSignedInUser();
     let userData = yield fxAccounts.getSignedInUser();
     isnot(userData, null, "Logged in now");
-    gFxAccounts.updateUI(); // Causes a leak (see bug 1332985)
+    gSync.updateAllUI(UIState.get());
     yield showMenuPromise("appMenu");
     yield showHighlightPromise("accountStatus");
     let highlight = document.getElementById("UITourHighlightContainer");
     is(highlight.popupBoxObject.anchorNode.id, "PanelUI-fxa-avatar", "Anchored on avatar");
     is(highlight.getAttribute("targetName"), "accountStatus", "Correct highlight target");
   }),
 ];
 
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -570,17 +570,18 @@ toolbarpaletteitem[place="palette"] > to
   display: none;
 }
 
 #PanelUI-footer-fxa:not([fxastatus="signedin"]) > toolbarseparator,
 #PanelUI-footer-fxa:not([fxastatus="signedin"]) > #PanelUI-fxa-icon {
   display: none;
 }
 
-#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status::after {
+#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status::after,
+#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status::after {
   content: url(chrome://browser/skin/warning.svg);
   filter: drop-shadow(0 1px 0 hsla(206,50%,10%,.15));
   width: 47px;
   padding-top: 1px;
   display: block;
   text-align: center;
   position: relative;
   top: 25%;
@@ -949,26 +950,29 @@ toolbarpaletteitem[place="palette"] > to
 
 #PanelUI-fxa-status:hover,
 #PanelUI-fxa-status:hover:active,
 #PanelUI-fxa-icon:hover,
 #PanelUI-fxa-icon:hover:active {
   outline: none;
 }
 
-#PanelUI-footer-fxa[fxastatus="error"] {
+#PanelUI-footer-fxa[fxastatus="login-failed"],
+#PanelUI-footer-fxa[fxastatus="unverified"] {
   background-color: hsl(42,94%,88%);
   border-top: 1px solid hsl(42,94%,70%);
 }
 
-#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover {
+#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status:hover,
+#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status:hover {
   background-color: hsl(42,94%,85%);
 }
 
-#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
+#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status:hover:active,
+#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status:hover:active {
   background-color: hsl(42,94%,82%);
   box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
 .PanelUI-notification-menu-item {
   color: black;
   background-color: hsla(96,65%,75%,.5);
 }
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -80,17 +80,19 @@ this.FxAccountsWebChannel = function(opt
   this._contentUri = options.content_uri;
 
   if (!options["channel_id"]) {
     throw new Error("Missing 'channel_id' option");
   }
   this._webChannelId = options.channel_id;
 
   // options.helpers is only specified by tests.
-  this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
+  XPCOMUtils.defineLazyGetter(this, "_helpers", () => {
+    return options.helpers || new FxAccountsWebChannelHelpers(options);
+  });
 
   this._setupChannel();
 };
 
 this.FxAccountsWebChannel.prototype = {
   /**
    * WebChannel that is used to communicate with content page
    */
--- a/services/sync/modules/SyncedTabs.jsm
+++ b/services/sync/modules/SyncedTabs.jsm
@@ -208,16 +208,24 @@ let SyncedTabsInternal = {
       case "nsPref:changed":
         Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED);
         break;
       default:
         break;
     }
   },
 
+  get loginFailed() {
+    if (!weaveXPCService.ready) {
+      log.debug("Sync isn't yet ready; assuming the login didn't fail");
+      return false;
+    }
+    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+  },
+
   // Returns true if Sync is configured to Sync tabs, false otherwise
   get isConfiguredToSyncTabs() {
     if (!weaveXPCService.ready) {
       log.debug("Sync isn't yet ready; assuming tab engine is enabled");
       return true;
     }
 
     let engine = Weave.Service.engineManager.get("tabs");
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/UIState.jsm
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ /**
+ * @typedef {Object} UIState
+ * @property {string} status The Sync/FxA status, see STATUS_* constants.
+ * @property {string} [email] The FxA email configured to log-in with Sync.
+ * @property {string} [displayName] The user's FxA display name.
+ * @property {string} [avatarURL] The user's FxA avatar URL.
+ * @property {Date} [lastSync] The last sync time.
+ * @property {boolean} [syncing] Whether or not we are currently syncing.
+ */
+
+this.EXPORTED_SYMBOLS = ["UIState"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+                                  "resource://services-sync/main.js");
+
+const TOPICS = [
+  "weave:service:login:change",
+  "weave:service:login:error",
+  "weave:service:ready",
+  "weave:service:sync:start",
+  "weave:service:sync:finish",
+  "weave:service:sync:error",
+  "fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
+  "fxaccounts:onlogout",
+  "fxaccounts:profilechange",
+];
+
+const ON_UPDATE = "sync-ui-state:update"
+
+const STATUS_NOT_CONFIGURED = "not_configured";
+const STATUS_LOGIN_FAILED = "login_failed";
+const STATUS_NOT_VERIFIED = "not_verified";
+const STATUS_SIGNED_IN = "signed_in";
+
+const DEFAULT_STATE = {
+  status: STATUS_NOT_CONFIGURED
+};
+
+const UIStateInternal = {
+  _initialized: false,
+  _state: null,
+
+  // We keep _syncing out of the state object because we can only track it
+  // using sync events and we can't determine it at any point in time.
+  _syncing: false,
+
+  get state() {
+    if (!this._state) {
+      return DEFAULT_STATE;
+    }
+    return Object.assign({}, this._state, { syncing: this._syncing });
+  },
+
+  isReady() {
+    if (!this._initialized) {
+      this.init();
+      return false;
+    }
+    return true;
+  },
+
+  init() {
+    this._initialized = true;
+    // Refresh the state in the background.
+    this.refreshState().catch(e => {
+      Cu.reportError(e);
+    });
+  },
+
+  // Used for testing.
+  reset() {
+    this._state = null;
+    this._syncing = false;
+    this._initialized = false;
+  },
+
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "weave:service:sync:start":
+        this.toggleSyncActivity(true);
+        break;
+      case "weave:service:sync:finish":
+      case "weave:service:sync:error":
+        this.toggleSyncActivity(false);
+        break;
+      default:
+        this.refreshState().catch(e => {
+          Cu.reportError(e);
+        });
+        break;
+    }
+  },
+
+  // Builds a new state from scratch.
+  async refreshState() {
+    this._state = {};
+    await this._refreshFxAState();
+    this._setLastSyncTime(this._state); // We want this in case we change accounts.
+
+    this.notifyStateUpdated();
+    return this.state;
+  },
+
+  // Update the current state with the last sync time/currently syncing status.
+  toggleSyncActivity(syncing) {
+    this._syncing = syncing;
+    this._setLastSyncTime(this._state);
+
+    this.notifyStateUpdated();
+  },
+
+  notifyStateUpdated() {
+    Services.obs.notifyObservers(null, ON_UPDATE);
+  },
+
+  async _refreshFxAState() {
+    let userData = await this._getUserData();
+    this._populateWithUserData(this._state, userData);
+    if (this.state.status != STATUS_SIGNED_IN) {
+      return;
+    }
+    let profile = await this._getProfile();
+    if (!profile) {
+      return;
+    }
+    this._populateWithProfile(this._state, profile);
+  },
+
+  _populateWithUserData(state, userData) {
+    let status;
+    if (!userData) {
+      status = STATUS_NOT_CONFIGURED;
+    } else {
+      if (this._loginFailed()) {
+        status = STATUS_LOGIN_FAILED;
+      } else if (!userData.verified) {
+        status = STATUS_NOT_VERIFIED;
+      } else {
+        status = STATUS_SIGNED_IN;
+      }
+      state.email = userData.email;
+    }
+    state.status = status;
+  },
+
+  _populateWithProfile(state, profile) {
+    state.displayName = profile.displayName;
+    state.avatarURL = profile.avatar;
+  },
+
+  async _getUserData() {
+    try {
+      return await this.fxAccounts.getSignedInUser();
+    } catch (e) {
+      // This is most likely in tests, where we quickly log users in and out.
+      // The most likely scenario is a user logged out, so reflect that.
+      // Bug 995134 calls for better errors so we could retry if we were
+      // sure this was the failure reason.
+      Cu.reportError("Error updating FxA account info: " + e);
+      return null;
+    }
+  },
+
+  async _getProfile() {
+    try {
+      return await this.fxAccounts.getSignedInUserProfile();
+    } catch (e) {
+      // Not fetching the profile is sad but the FxA logs will already have noise.
+      return null;
+    }
+  },
+
+  _setLastSyncTime(state) {
+    if (state.status == UIState.STATUS_SIGNED_IN) {
+      try {
+        state.lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync", null));
+      } catch (_) {
+        state.lastSync = null;
+      }
+    }
+  },
+
+  _loginFailed() {
+    // Referencing Weave.Service will implicitly initialize sync, and we don't
+    // want to force that - so first check if it is ready.
+    let service = Cc["@mozilla.org/weave/service;1"]
+                  .getService(Ci.nsISupports)
+                  .wrappedJSObject;
+    if (!service.ready) {
+      return false;
+    }
+    // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
+    // All other login failures are assumed to be transient and should go
+    // away by themselves, so aren't reflected here.
+    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+  },
+
+  set fxAccounts(mockFxAccounts) {
+    delete this.fxAccounts;
+    this.fxAccounts = mockFxAccounts;
+  }
+};
+
+XPCOMUtils.defineLazyModuleGetter(UIStateInternal, "fxAccounts",
+                                  "resource://gre/modules/FxAccounts.jsm");
+
+for (let topic of TOPICS) {
+  Services.obs.addObserver(UIStateInternal, topic);
+}
+
+this.UIState = {
+  _internal: UIStateInternal,
+
+  ON_UPDATE,
+
+  STATUS_NOT_CONFIGURED,
+  STATUS_LOGIN_FAILED,
+  STATUS_NOT_VERIFIED,
+  STATUS_SIGNED_IN,
+
+  /**
+   * Returns true if the module has been initialized and the state set.
+   * If not, return false and trigger an init in the background.
+   */
+  isReady() {
+    return this._internal.isReady();
+  },
+
+  /**
+   * @returns {UIState} The current Sync/FxA UI State.
+   */
+  get() {
+    return this._internal.state;
+  },
+
+  /**
+   * Refresh the state. Used for testing, don't call this directly since
+   * UIState already listens to Sync/FxA notifications to determine if the state
+   * needs to be refreshed. ON_UPDATE will be fired once the state is refreshed.
+   *
+   * @returns {Promise<UIState>} Resolved once the state is refreshed.
+   */
+  refresh() {
+    return this._internal.refreshState();
+  },
+
+  /**
+   * Reset the state of the whole module. Used for testing.
+   */
+  reset() {
+    this._internal.reset();
+  }
+};
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -31,16 +31,17 @@ EXTRA_JS_MODULES['services-sync'] += [
     'modules/policies.js',
     'modules/record.js',
     'modules/resource.js',
     'modules/rest.js',
     'modules/service.js',
     'modules/status.js',
     'modules/SyncedTabs.jsm',
     'modules/telemetry.js',
+    'modules/UIState.jsm',
     'modules/util.js',
 ]
 
 EXTRA_PP_JS_MODULES['services-sync'] += [
     'modules/constants.js',
 ]
 
 # Definitions used by constants.js
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_uistate.js
@@ -0,0 +1,260 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// ================================================
+// Load mocking/stubbing library, sinon
+// docs: http://sinonjs.org/docs/
+/* global sinon */
+Cu.import("resource://gre/modules/Timer.jsm");
+let window = {
+  document: {},
+  location: {},
+  setTimeout,
+  setInterval,
+  clearTimeout,
+  clearInterval,
+};
+let self = window;
+let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
+loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
+// ================================================
+
+Cu.import("resource://services-sync/UIState.jsm");
+
+const UIStateInternal = UIState._internal;
+
+add_task(async function test_isReady() {
+  UIState.reset();
+
+  let refreshState = sinon.spy(UIStateInternal, "refreshState");
+
+  // On the first call, returns false and triggers a refresh of the state
+  ok(!UIState.isReady());
+  ok(refreshState.calledOnce);
+  refreshState.reset();
+
+  // On subsequent calls, only return true
+  ok(UIState.isReady());
+  ok(!refreshState.called);
+
+  refreshState.restore();
+});
+
+add_task(async function test_refreshState_signedin() {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  const now = new Date().toString();
+  Services.prefs.setCharPref("services.sync.lastSync", now);
+  UIStateInternal.syncing = false;
+
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+    getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" })
+  }
+
+  let state = await UIState.refresh();
+
+  equal(state.status, UIState.STATUS_SIGNED_IN);
+  equal(state.email, "foo@bar.com");
+  equal(state.displayName, "Foo Bar");
+  equal(state.avatarURL, "https://foo/bar");
+  equal(state.lastSync, now);
+  equal(state.syncing, false);
+
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_signedin_profile_unavailable() {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  const now = new Date().toString();
+  Services.prefs.setCharPref("services.sync.lastSync", now);
+  UIStateInternal.syncing = false;
+
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+    getSignedInUserProfile: () => Promise.reject(new Error("Profile unavailable"))
+  }
+
+  let state = await UIState.refresh();
+
+  equal(state.status, UIState.STATUS_SIGNED_IN);
+  equal(state.email, "foo@bar.com");
+  equal(state.displayName, undefined);
+  equal(state.avatarURL, undefined);
+  equal(state.lastSync, now);
+  equal(state.syncing, false);
+
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_unconfigured() {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  let getSignedInUserProfile = sinon.spy();
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve(null),
+    getSignedInUserProfile
+  }
+
+  let state = await UIState.refresh();
+
+  equal(state.status, UIState.STATUS_NOT_CONFIGURED);
+  equal(state.email, undefined);
+  equal(state.displayName, undefined);
+  equal(state.avatarURL, undefined);
+  equal(state.lastSync, undefined);
+
+  ok(!getSignedInUserProfile.called);
+
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_unverified() {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  let getSignedInUserProfile = sinon.spy();
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve({ verified: false, email: "foo@bar.com" }),
+    getSignedInUserProfile
+  }
+
+  let state = await UIState.refresh();
+
+  equal(state.status, UIState.STATUS_NOT_VERIFIED);
+  equal(state.email, "foo@bar.com");
+  equal(state.displayName, undefined);
+  equal(state.avatarURL, undefined);
+  equal(state.lastSync, undefined);
+
+  ok(!getSignedInUserProfile.called);
+
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_loginFailed() {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  let loginFailed = sinon.stub(UIStateInternal, "_loginFailed");
+  loginFailed.returns(true);
+
+  let getSignedInUserProfile = sinon.spy();
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+    getSignedInUserProfile
+  }
+
+  let state = await UIState.refresh();
+
+  equal(state.status, UIState.STATUS_LOGIN_FAILED);
+  equal(state.email, "foo@bar.com");
+  equal(state.displayName, undefined);
+  equal(state.avatarURL, undefined);
+  equal(state.lastSync, undefined);
+
+  ok(!getSignedInUserProfile.called);
+
+  loginFailed.restore();
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_observer_refreshState() {
+  let refreshState = sinon.spy(UIStateInternal, "refreshState");
+
+  let shouldRefresh = ["weave:service:login:change", "weave:service:login:error",
+                       "weave:service:ready", "fxaccounts:onlogin",
+                       "fxaccounts:onlogout", "fxaccounts:profilechange"];
+
+  for (let topic of shouldRefresh) {
+    let uiUpdateObserved = observeUIUpdate();
+    Services.obs.notifyObservers(null, topic);
+    await uiUpdateObserved;
+    ok(refreshState.calledOnce);
+    refreshState.reset();
+  }
+
+  refreshState.restore();
+});
+
+// Drive the UIState in a configured state.
+async function configureUIState(syncing, lastSync = new Date()) {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  UIStateInternal._syncing = syncing;
+  Services.prefs.setCharPref("services.sync.lastSync", lastSync.toString());
+
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+    getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" })
+  }
+  await UIState.refresh();
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+}
+
+add_task(async function test_syncStarted() {
+  await configureUIState(false);
+
+  const oldState = Object.assign({}, UIState.get());
+  ok(!oldState.syncing);
+
+  let uiUpdateObserved = observeUIUpdate();
+  Services.obs.notifyObservers(null, "weave:service:sync:start");
+  await uiUpdateObserved;
+
+  const newState = Object.assign({}, UIState.get());
+  ok(newState.syncing);
+});
+
+add_task(async function test_syncFinished() {
+  let yesterday = new Date();
+  yesterday.setDate(yesterday.getDate() - 1);
+  await configureUIState(true, yesterday);
+
+  const oldState = Object.assign({}, UIState.get());
+  ok(oldState.syncing);
+
+  let uiUpdateObserved = observeUIUpdate();
+  Services.prefs.setCharPref("services.sync.lastSync", new Date().toString());
+  Services.obs.notifyObservers(null, "weave:service:sync:finish");
+  await uiUpdateObserved;
+
+  const newState = Object.assign({}, UIState.get());
+  ok(!newState.syncing);
+  ok(new Date(newState.lastSync) > new Date(oldState.lastSync));
+});
+
+add_task(async function test_syncError() {
+  let yesterday = new Date();
+  yesterday.setDate(yesterday.getDate() - 1);
+  await configureUIState(true, yesterday);
+
+  const oldState = Object.assign({}, UIState.get());
+  ok(oldState.syncing);
+
+  let uiUpdateObserved = observeUIUpdate();
+  Services.obs.notifyObservers(null, "weave:service:sync:error");
+  await uiUpdateObserved;
+
+  const newState = Object.assign({}, UIState.get());
+  ok(!newState.syncing);
+  deepEqual(newState.lastSync, oldState.lastSync);
+});
+
+function observeUIUpdate() {
+  return new Promise(resolve => {
+    let obs = (aSubject, aTopic, aData) => {
+      Services.obs.removeObserver(obs, aTopic);
+      const state = UIState.get();
+      resolve(state);
+    }
+    Services.obs.addObserver(obs, UIState.ON_UPDATE);
+  });
+}
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -179,8 +179,10 @@ support-files = prefs_test_prefs_store.j
 [test_warn_on_truncated_response.js]
 [test_postqueue.js]
 
 # Synced tabs.
 [test_syncedtabs.js]
 
 [test_telemetry.js]
 requesttimeoutfactor = 4
+
+[test_uistate.js]