Bug 1353571 part 3 - Refactor browser-syncui and browser-fxaccounts. r=markh
authorEdouard Oger <eoger@fastmail.com>
Tue, 18 Apr 2017 14:15:43 -0400
changeset 356410 619b0f41a72ad072b5601169201d0cf95288674b
parent 356409 6f30e9012fc9e731c7d47f8879891e26aa92b579
child 356411 b54bfb672a90c6ac0373488aab29c0215493c15a
push id31763
push usercbook@mozilla.com
push dateThu, 04 May 2017 08:55:02 +0000
treeherdermozilla-central@a8d597ee6dd5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1353571
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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 100644
--- 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
@@ -207,16 +207,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]