Bug 1269277 - Add Manage Device button in Synced Tabs menu/sidebar. r?markh draft
authorEdouard Oger <eoger@fastmail.com>
Fri, 10 Mar 2017 20:14:51 -0500
changeset 496980 bc55701020d0ea0006f1b7fb683cf23d8c132f58
parent 496979 4ceb9062ea8f4113bfd1b3536ace4a840a72faa7
child 548773 bb81efa3a6d71af87e7ee9560dd1aebbb2924d2a
push id48767
push userbmo:eoger@fastmail.com
push dateSat, 11 Mar 2017 01:30:06 +0000
reviewersmarkh
bugs1269277
milestone55.0a1
Bug 1269277 - Add Manage Device button in Synced Tabs menu/sidebar. r?markh MozReview-Commit-ID: 2BMgLeI0DNF
browser/base/content/browser-fxaccounts.js
browser/base/content/browser.xul
browser/base/content/test/general/browser_aboutAccounts.js
browser/components/customizableui/content/panelUI.inc.xul
browser/components/nsBrowserGlue.js
browser/components/syncedtabs/TabListView.js
browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
browser/locales/en-US/chrome/browser/browser.dtd
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/tests/browser/browser_device_connected.js
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -318,16 +318,23 @@ var gFxAccounts = {
       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();
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -473,16 +473,20 @@
                 id="syncedTabsBookmarkSelected"/>
       <menuitem label="&syncedTabs.context.copy.label;"
                 accesskey="&syncedTabs.context.copy.accesskey;"
                 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');"/>
       <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/test/general/browser_aboutAccounts.js
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -195,23 +195,23 @@ var gTests = [
 },
 {
   desc: "Test action=reauth",
   *teardown() {
     gBrowser.removeCurrentTab();
     yield signOut();
   },
   *run() {
-    const expected_url = "https://example.com/?is_force_auth";
+    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 + "&email=foo%40example.com";
+    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();
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -109,16 +109,20 @@
         <!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
         <!-- When Sync is ready to sync -->
         <vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state">
           <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');"/>
             <toolbarbutton id="PanelUI-remotetabs-syncnow"
                            observes="sync-status"
                            class="subviewbutton"
                            oncommand="gSyncUI.doSync();"
                            closemenu="none"/>
             <menuseparator id="PanelUI-remotetabs-separator"/>
           </vbox>
           <deck id="PanelUI-remotetabs-deck">
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -143,16 +143,18 @@ function BrowserGlue() {
 
   XPCOMUtils.defineLazyGetter(this, "_sanitizer",
     function() {
       let sanitizerScope = {};
       Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", sanitizerScope);
       return sanitizerScope.Sanitizer;
     });
 
+  XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
+
   this._init();
 }
 
 /*
  * OS X has the concept of zero-window sessions and therefore ignores the
  * browser-lastwindow-close-* topics.
  */
 const OBSERVE_LASTWINDOW_CLOSE_TOPICS = AppConstants.platform != "macosx";
@@ -300,16 +302,20 @@ BrowserGlue.prototype = {
           this._distributionCustomizer.applyCustomizations();
           // To apply distribution bookmarks use "places-init-complete".
         } else if (data == "force-places-init") {
           this._initPlaces(false);
         } else if (data == "smart-bookmarks-init") {
           this.ensurePlacesDefaultQueriesInitialized().then(() => {
             Services.obs.notifyObservers(null, "test-smart-bookmarks-done", null);
           });
+        } else if (data == "mock-fxaccounts") {
+          Object.defineProperty(this, "fxAccounts", {
+            value: subject.wrappedJSObject
+          });
         }
         break;
       case "initial-migration-will-import-default-bookmarks":
         this._migrationImportsDefaultBookmarks = true;
         break;
       case "initial-migration-did-import-default-bookmarks":
         this._initPlaces(true);
         break;
@@ -2297,31 +2303,31 @@ BrowserGlue.prototype = {
   _onDeviceConnected(deviceName) {
     let accountsBundle = Services.strings.createBundle(
       "chrome://browser/locale/accounts.properties"
     );
     let title = accountsBundle.GetStringFromName("deviceConnectedTitle");
     let body = accountsBundle.formatStringFromName("deviceConnectedBody" +
                                                    (deviceName ? "" : ".noDeviceName"),
                                                    [deviceName], 1);
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.devices.uri");
 
-    function clickCallback(subject, topic, data) {
+    let clickCallback = async (subject, topic, data) => {
       if (topic != "alertclickcallback")
         return;
+      let url = await this.fxAccounts.promiseAccountsManageDevicesURI("device-connected-notification");
       let win = RecentWindow.getMostRecentBrowserWindow({private: false});
       if (!win) {
         Services.appShell.hiddenDOMWindow.open(url);
       } else {
         win.gBrowser.addTab(url);
       }
-    }
+    };
 
     try {
-      AlertsService.showAlertNotification(null, title, body, true, url, clickCallback);
+      AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
     } catch (ex) {
       Cu.reportError("Error notifying of a new Sync device: " + ex);
     }
   },
 
   _onDeviceDisconnected() {
     let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
     let title = bundle.GetStringFromName("deviceDisconnectedNotification.title");
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -519,24 +519,27 @@ TabListView.prototype = {
     let item = this.container.querySelector(".item.selected");
     let showTabOptions = this._isTab(item);
 
     let el = menu.firstChild;
 
     while (el) {
       let show = false;
       if (showTabOptions) {
-        if (el.getAttribute("id") != "syncedTabsOpenAllInTabs") {
+        if (el.getAttribute("id") != "syncedTabsOpenAllInTabs" &&
+            el.getAttribute("id") != "syncedTabsManageDevices") {
           show = true;
         }
       } else if (el.getAttribute("id") == "syncedTabsOpenAllInTabs") {
         const tabs = item.querySelectorAll(".item-tabs-list > .item.tab");
         show = tabs.length > 0;
       } else if (el.getAttribute("id") == "syncedTabsRefresh") {
         show = true;
+      } else if (el.getAttribute("id") == "syncedTabsManageDevices") {
+        show = true;
       }
       el.hidden = !show;
 
       el = el.nextSibling;
     }
   },
 
   /**
--- a/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
+++ b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
@@ -305,34 +305,36 @@ add_task(function* testSyncedTabsSidebar
     ["menuitem#syncedTabsOpenSelectedInTab", { hidden: false }],
     ["menuitem#syncedTabsOpenSelectedInWindow", { hidden: false }],
     ["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: false }],
     ["menuseparator", { hidden: false }],
     ["menuitem#syncedTabsBookmarkSelected", { hidden: false }],
     ["menuitem#syncedTabsCopySelected", { hidden: false }],
     ["menuseparator", { hidden: false }],
     ["menuitem#syncedTabsOpenAllInTabs", { hidden: true }],
+    ["menuitem#syncedTabsManageDevices", { hidden: true }],
     ["menuitem#syncedTabsRefresh", { hidden: false }],
   ];
   yield* testContextMenu(syncedTabsDeckComponent,
                          "#SyncedTabsSidebarContext",
                          "#tab-7cqCr77ptzX3-0",
                          tabMenuItems);
 
-  info("Right-clicking a client should show the Open All in Tabs action");
+  info("Right-clicking a client should show the Open All in Tabs and Manage devices actions");
   let sidebarMenuItems = [
     ["menuitem#syncedTabsOpenSelected", { hidden: true }],
     ["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }],
     ["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }],
     ["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }],
     ["menuseparator", { hidden: true }],
     ["menuitem#syncedTabsBookmarkSelected", { hidden: true }],
     ["menuitem#syncedTabsCopySelected", { hidden: true }],
     ["menuseparator", { hidden: true }],
     ["menuitem#syncedTabsOpenAllInTabs", { hidden: false }],
+    ["menuitem#syncedTabsManageDevices", { hidden: false }],
     ["menuitem#syncedTabsRefresh", { hidden: false }],
   ];
   yield* testContextMenu(syncedTabsDeckComponent,
                          "#SyncedTabsSidebarContext",
                          "#item-7cqCr77ptzX3",
                          sidebarMenuItems);
 
   info("Right-clicking a client without any tabs should not show the Open All in Tabs action");
@@ -341,16 +343,17 @@ add_task(function* testSyncedTabsSidebar
     ["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }],
     ["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }],
     ["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }],
     ["menuseparator", { hidden: true }],
     ["menuitem#syncedTabsBookmarkSelected", { hidden: true }],
     ["menuitem#syncedTabsCopySelected", { hidden: true }],
     ["menuseparator", { hidden: true }],
     ["menuitem#syncedTabsOpenAllInTabs", { hidden: true }],
+    ["menuitem#syncedTabsManageDevices", { hidden: false }],
     ["menuitem#syncedTabsRefresh", { hidden: false }],
   ];
   yield* testContextMenu(syncedTabsDeckComponent,
                          "#SyncedTabsSidebarContext",
                          "#item-OL3EJCsdb2JD",
                          menuItems);
 });
 
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -372,16 +372,17 @@ These should match what Safari and other
 <!-- LOCALIZATION NOTE (appMenuRemoteTabs.noclients.label): This is shown
      when Sync is configured but this appears to be the only device attached to
      the account. We also show links to download Firefox for android/ios. -->
 <!ENTITY appMenuRemoteTabs.noclients.title "No synced tabs… yet!">
 <!ENTITY appMenuRemoteTabs.noclients.subtitle "Want to see your tabs from other devices here?">
 <!ENTITY appMenuRemoteTabs.openprefs.label "Sync Preferences">
 <!ENTITY appMenuRemoteTabs.notsignedin.label "Sign in to view a list of tabs from your other devices.">
 <!ENTITY appMenuRemoteTabs.signin.label "Sign in to Sync">
+<!ENTITY appMenuRemoteTabs.managedevices.label "Manage Devices…">
 <!ENTITY appMenuRemoteTabs.sidebar.label "View Synced Tabs Sidebar">
 
 <!ENTITY customizeMenu.addToToolbar.label "Add to Toolbar">
 <!ENTITY customizeMenu.addToToolbar.accesskey "A">
 <!ENTITY customizeMenu.addToPanel.label "Add to Menu">
 <!ENTITY customizeMenu.addToPanel.accesskey "M">
 <!ENTITY customizeMenu.moveToToolbar.label "Move to Toolbar">
 <!ENTITY customizeMenu.moveToToolbar.accesskey "o">
@@ -780,16 +781,18 @@ you can use these alternative items. Oth
 <!ENTITY syncedTabs.context.openInNewPrivateWindow.accesskey "P">
 <!ENTITY syncedTabs.context.bookmarkSingleTab.label          "Bookmark This Tab…">
 <!ENTITY syncedTabs.context.bookmarkSingleTab.accesskey      "B">
 <!ENTITY syncedTabs.context.copy.label                       "Copy">
 <!ENTITY syncedTabs.context.copy.accesskey                   "C">
 
 <!ENTITY syncedTabs.context.openAllInTabs.label              "Open All in Tabs">
 <!ENTITY syncedTabs.context.openAllInTabs.accesskey          "O">
+<!ENTITY syncedTabs.context.managedevices.label              "Manage Devices…">
+<!ENTITY syncedTabs.context.managedevices.accesskey          "D">
 
 
 <!ENTITY syncBrand.shortName.label    "Sync">
 
 <!ENTITY syncSignIn.label             "Sign In To &syncBrand.shortName.label;…">
 <!ENTITY syncSignIn.accesskey         "Y">
 <!ENTITY syncSyncNowItem.label        "Sync Now">
 <!ENTITY syncSyncNowItem.accesskey    "S">
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -2,16 +2,18 @@
  * 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";
 
 this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.importGlobalProperties(["URL"]);
+
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
@@ -54,16 +56,17 @@ var publicProperties = [
   "invalidateCertificate",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "notifyDevices",
   "now",
   "promiseAccountsChangeProfileURI",
   "promiseAccountsForceSigninURI",
   "promiseAccountsManageURI",
+  "promiseAccountsManageDevicesURI",
   "promiseAccountsSignUpURI",
   "promiseAccountsSignInURI",
   "removeCachedOAuthToken",
   "requiresHttps",
   "resendVerificationEmail",
   "resetCredentials",
   "sessionStatus",
   "setProfileCache",
@@ -1258,90 +1261,76 @@ FxAccountsInternal.prototype = {
   promiseAccountsSignUpURI() {
     return FxAccountsConfig.promiseAccountsSignUpURI();
   },
 
   promiseAccountsSignInURI() {
     return FxAccountsConfig.promiseAccountsSignInURI();
   },
 
-  // Returns a promise that resolves with the URL to use to force a re-signin
-  // of the current account.
-  promiseAccountsForceSigninURI: Task.async(function *() {
-    yield FxAccountsConfig.ensureConfigured();
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri");
-    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+  /**
+   * Pull an URL defined in the user preferences, add the current UID and email
+   * to the query string, add entrypoint and extra params to the query string if
+   * requested.
+   * @param {string} prefName The preference name from where to pull the URL to format.
+   * @param {string} [entrypoint] "entrypoint" searchParam value.
+   * @param {Object.<string, string>} [extraParams] Additionnal searchParam key and values.
+   * @returns {Promise.<string>} A promise that resolves to the formatted URL
+   */
+  async _formatPrefURL(prefName, entrypoint, extraParams) {
+    let url = new URL(Services.urlFormatter.formatURLPref(prefName));
+    if (this.requiresHttps() && url.protocol != "https:") {
       throw new Error("Firefox Accounts server must use HTTPS");
     }
-    let currentState = this.currentAccountState;
-    // but we need to append the email address onto a query string.
-    return this.getSignedInUser().then(accountData => {
-      if (!accountData) {
-        return null;
+    let accountData = await this.getSignedInUser();
+    if (!accountData) {
+      return Promise.resolve(null);
+    }
+    url.searchParams.append("uid", accountData.uid);
+    url.searchParams.append("email", accountData.email);
+    if (entrypoint) {
+      url.searchParams.append("entrypoint", entrypoint);
+    }
+    if (extraParams) {
+      for (let [k, v] of Object.entries(extraParams)) {
+        url.searchParams.append(k, v);
       }
-      let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
-      newQueryPortion += "email=" + encodeURIComponent(accountData.email);
-      return url + newQueryPortion;
-    }).then(result => currentState.resolve(result));
-  }),
+    }
+    return this.currentAccountState.resolve(url.href);
+  },
+
+  // Returns a promise that resolves with the URL to use to force a re-signin
+  // of the current account.
+  async promiseAccountsForceSigninURI() {
+    await FxAccountsConfig.ensureConfigured();
+    return this._formatPrefURL("identity.fxaccounts.remote.force_auth.uri");
+  },
 
   // Returns a promise that resolves with the URL to use to change
   // the current account's profile image.
   // if settingToEdit is set, the profile page should hightlight that setting
   // for the user to edit.
-  promiseAccountsChangeProfileURI(entrypoint, settingToEdit = null) {
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
-
+  async promiseAccountsChangeProfileURI(entrypoint, settingToEdit = null) {
+    let extraParams;
     if (settingToEdit) {
-      url += (url.indexOf("?") == -1 ? "?" : "&") +
-             "setting=" + encodeURIComponent(settingToEdit);
-    }
-
-    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
+      extraParams = { setting: settingToEdit };
     }
-    let currentState = this.currentAccountState;
-    // but we need to append the email address onto a query string.
-    return this.getSignedInUser().then(accountData => {
-      if (!accountData) {
-        return null;
-      }
-      let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
-      newQueryPortion += "email=" + encodeURIComponent(accountData.email);
-      newQueryPortion += "&uid=" + encodeURIComponent(accountData.uid);
-      if (entrypoint) {
-        newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint);
-      }
-      return url + newQueryPortion;
-    }).then(result => currentState.resolve(result));
+    return this._formatPrefURL("identity.fxaccounts.settings.uri", entrypoint, extraParams);
   },
 
   // Returns a promise that resolves with the URL to use to manage the current
   // user's FxA acct.
-  promiseAccountsManageURI(entrypoint) {
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
-    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    let currentState = this.currentAccountState;
-    // but we need to append the uid and email address onto a query string
-    // (if the server has no matching uid it will offer to sign in with the
-    // email address)
-    return this.getSignedInUser().then(accountData => {
-      if (!accountData) {
-        return null;
-      }
-      let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
-      newQueryPortion += "uid=" + encodeURIComponent(accountData.uid) +
-                         "&email=" + encodeURIComponent(accountData.email);
-      if (entrypoint) {
-        newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint);
-      }
-      return url + newQueryPortion;
-    }).then(result => currentState.resolve(result));
+  async promiseAccountsManageURI(entrypoint) {
+    return this._formatPrefURL("identity.fxaccounts.settings.uri", entrypoint);
+  },
+
+  // Returns a promise that resolves with the URL to use to manage the devices in
+  // the current user's FxA acct.
+  async promiseAccountsManageDevicesURI(entrypoint) {
+    return this._formatPrefURL("identity.fxaccounts.settings.devices.uri", entrypoint);
   },
 
   /**
    * Get an OAuth token for the user
    *
    * @param options
    *        {
    *          scope: (string/array) the oauth scope(s) being requested. As a
--- a/services/fxaccounts/tests/browser/browser_device_connected.js
+++ b/services/fxaccounts/tests/browser/browser_device_connected.js
@@ -1,23 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const { MockRegistrar } =
   Cu.import("resource://testing-common/MockRegistrar.jsm", {});
-
-let accountsBundle = Services.strings.createBundle(
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"]
+                     .getService(Ci.nsIObserver);
+const accountsBundle = Services.strings.createBundle(
   "chrome://browser/locale/accounts.properties"
 );
+const DEVICES_URL = "http://localhost/devices";
 
 let expectedBody;
 
 add_task(async function setup() {
+  let fxAccounts = {
+    promiseAccountsManageDevicesURI() {
+      return Promise.resolve(DEVICES_URL);
+    }
+  };
+  gBrowserGlue.observe({wrappedJSObject: fxAccounts}, "browser-glue-test", "mock-fxaccounts");
   const alertsService = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService, Ci.nsISupports]),
     showAlertNotification: (image, title, text, clickable, cookie, clickCallback) => {
       // We can't simulate a click on the alert popup,
       // so instead we call the click listener ourselves directly
       clickCallback.observe(null, "alertclickcallback", null);
       Assert.equal(text, expectedBody);
     }
@@ -28,28 +36,24 @@ add_task(async function setup() {
   });
 });
 
 async function testDeviceConnected(deviceName) {
   info("testDeviceConnected with deviceName=" + deviceName);
   gBrowser.selectedBrowser.loadURI("about:robots");
   await waitForDocLoadComplete();
 
-  Preferences.set("identity.fxaccounts.settings.devices.uri", "http://localhost/devices");
-
   let waitForTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
 
   Services.obs.notifyObservers(null, "fxaccounts:device_connected", deviceName);
 
   let tab = await waitForTabPromise;
   Assert.ok("Tab successfully opened");
 
-  let expectedURI = Preferences.get("identity.fxaccounts.settings.devices.uri",
-                                    "prefundefined");
-  Assert.equal(tab.linkedBrowser.currentURI.spec, expectedURI);
+  Assert.equal(tab.linkedBrowser.currentURI.spec, DEVICES_URL);
 
   await BrowserTestUtils.removeTab(tab);
 }
 
 add_task(async function() {
   expectedBody = accountsBundle.formatStringFromName("deviceConnectedBody", ["My phone"], 1);
   await testDeviceConnected("My phone");
 });