Bug 1516997 p2 - Use hasSyncedThisSession to determine if Send Tab is configured and loading. r=markh
authorEdouard Oger <eoger@fastmail.com>
Thu, 03 Jan 2019 22:29:03 +0000
changeset 509590 7be73b4e5299792d47667c8587d56a5b2e36c71e
parent 509589 98d8d27b7fdfb53f562ce4372e5b022dbe3f6a37
child 509603 ce9872f98d6274a8db6592cc8f0ad19ea332fafd
child 509604 129f8c60783c4daba0c04106c3082c2f94890b66
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1516997
milestone66.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 1516997 p2 - Use hasSyncedThisSession to determine if Send Tab is configured and loading. r=markh Depends on D15657 Differential Revision: https://phabricator.services.mozilla.com/D15658
browser/base/content/browser-pageActions.js
browser/base/content/browser-sync.js
browser/base/content/test/sync/browser_contextmenu_sendpage.js
browser/base/content/test/sync/browser_contextmenu_sendtab.js
browser/base/content/test/sync/head.js
browser/base/content/test/urlbar/browser_page_action_menu.js
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -1060,17 +1060,19 @@ BrowserPageActions.sendToDevice = {
     gSync.populateSendTabToDevicesMenu(bodyNode, url, title, multiselected, (clientId, name, clientType, lastModified) => {
       if (!name) {
         return document.createXULElement("toolbarseparator");
       }
       let item = document.createXULElement("toolbarbutton");
       item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
       if (clientId) {
         item.classList.add("subviewbutton-iconic");
-        item.setAttribute("tooltiptext", gSync.formatLastSyncDate(lastModified));
+        if (lastModified) {
+          item.setAttribute("tooltiptext", gSync.formatLastSyncDate(lastModified));
+        }
       }
 
       item.addEventListener("command", event => {
         if (panelNode) {
           PanelMultiView.hidePopup(panelNode);
         }
         // There are items in the subview that don't represent devices: "Sign
         // in", "Learn about Sync", etc.  Device items will be .sendtab-target.
@@ -1081,24 +1083,24 @@ BrowserPageActions.sendToDevice = {
         }
       });
       return item;
     });
 
     bodyNode.removeAttribute("state");
     // In the first ~10 sec after startup, Sync may not be loaded and the list
     // of devices will be empty.
-    if (gSync.syncConfiguredAndLoading) {
+    if (gSync.sendTabConfiguredAndLoading) {
       bodyNode.setAttribute("state", "notready");
       // Force a background Sync
       Services.tm.dispatchToMainThread(async () => {
         await Weave.Service.sync({why: "pageactions", engines: []}); // [] = clients engine only
         // There's no way Sync is still syncing at this point, but we check
         // anyway to avoid infinite looping.
-        if (!window.closed && !gSync.syncConfiguredAndLoading) {
+        if (!window.closed && !gSync.sendTabConfiguredAndLoading) {
           this.onShowingSubview(panelViewNode);
         }
       });
     }
   },
 };
 
 // add search engine
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -44,21 +44,21 @@ var gSync = {
       "chrome://weave/locale/sync.properties"
     );
   },
 
   get syncReady() {
     return Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject.ready;
   },
 
-  // Returns true if sync is configured but hasn't loaded or is yet to determine
-  // if any remote clients exist.
-  get syncConfiguredAndLoading() {
+  // Returns true if sync is configured but hasn't loaded or the send tab
+  // targets list isn't ready yet.
+  get sendTabConfiguredAndLoading() {
     return UIState.get().status == UIState.STATUS_SIGNED_IN &&
-           (!this.syncReady || Weave.Service.clientsEngine.isFirstSync);
+           (!this.syncReady || !Weave.Service.clientsEngine.hasSyncedThisSession);
   },
 
   get isSignedIn() {
     return UIState.get().status == UIState.STATUS_SIGNED_IN;
   },
 
   get sendTabTargets() {
     return Weave.Service.clientsEngine.fxaDevices
@@ -363,17 +363,17 @@ var gSync = {
     // remove existing menu items
     for (let i = devicesPopup.children.length - 1; i >= 0; --i) {
       let child = devicesPopup.children[i];
       if (child.classList.contains("sync-menuitem")) {
         child.remove();
       }
     }
 
-    if (gSync.syncConfiguredAndLoading) {
+    if (gSync.sendTabConfiguredAndLoading) {
       // We can only be in this case in the page action menu.
       return;
     }
 
     const fragment = document.createDocumentFragment();
 
     const state = UIState.get();
     if (state.status == UIState.STATUS_SIGNED_IN && this.sendTabTargets.length > 0) {
@@ -537,17 +537,17 @@ var gSync = {
     }
     let hasASendableURI = false;
     for (let tab of aTargetTab.multiselected ? gBrowser.selectedTabs : [aTargetTab]) {
       if (this.isSendableURI(tab.linkedBrowser.currentURI.spec)) {
         hasASendableURI = true;
         break;
       }
     }
-    const enabled = !this.syncConfiguredAndLoading && hasASendableURI;
+    const enabled = !this.sendTabConfiguredAndLoading && hasASendableURI;
 
     let sendTabsToDevice = document.getElementById("context_sendTabToDevice");
     sendTabsToDevice.disabled = !enabled;
 
     let tabCount = aTargetTab.multiselected ? gBrowser.multiSelectedTabsCount : 1;
     sendTabsToDevice.label = PluralForm.get(tabCount,
                                            gNavigatorBundle.getString("sendTabsToDevice.label"))
                                       .replace("#1", tabCount.toLocaleString());
@@ -577,17 +577,17 @@ var gSync = {
     .forEach(id => contextMenu.showItem(id, showSendLink));
 
     if (!showSendLink && !showSendPage) {
       return;
     }
 
     const targetURI = showSendLink ? contextMenu.linkURL :
                                      contextMenu.browser.currentURI.spec;
-    const enabled = !this.syncConfiguredAndLoading && this.isSendableURI(targetURI);
+    const enabled = !this.sendTabConfiguredAndLoading && this.isSendableURI(targetURI);
     contextMenu.setItemAttr(showSendPage ? "context-sendpagetodevice" :
                                            "context-sendlinktodevice",
                                            "disabled", !enabled || null);
   },
 
   // Functions called by observers
   onActivityStart() {
     clearTimeout(this._syncAnimationTimer);
--- a/browser/base/content/test/sync/browser_contextmenu_sendpage.js
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -1,48 +1,50 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const targetsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+const fxaDevices = [
+  {id: 1, name: "Foo", availableCommands: {"https://identity.mozilla.com/cmd/open-uri": "baz"}},
+  {id: 2, name: "Bar", clientRecord: "bar"}, // Legacy send tab target (no availableCommands).
+  {id: 3, name: "Homer"}, // Incompatible target.
+];
 
 add_task(async function setup() {
   await promiseSyncReady();
   // gSync.init() is called in a requestIdleCallback. Force its initialization.
   gSync.init();
   sinon.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop");
   await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
 });
 
 add_task(async function test_page_contextmenu() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture,
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
+    { label: "Bar" },
     { label: "Foo" },
-    { label: "Bar" },
     "----",
     { label: "Send to All Devices" },
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_link_contextmenu() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture,
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices});
   let expectation = sandbox.mock(gSync)
                            .expects("sendTabToDevice")
                            .once()
-                           .withExactArgs("https://www.example.org/", [{id: 1, name: "Foo"}], "Click on me!!");
+                           .withExactArgs("https://www.example.org/", [fxaDevices[1]], "Click on me!!");
 
   // Add a link to the page
   await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
     let a = content.document.createElement("a");
     a.href = "https://www.example.org";
     a.id = "testingLink";
     a.textContent = "Click on me!!";
     content.document.body.appendChild(a);
@@ -54,107 +56,100 @@ add_task(async function test_link_contex
   document.getElementById("context-sendlinktodevice-popup").querySelector("menuitem").click();
   await hideContentContextMenu();
 
   expectation.verify();
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_no_remote_clients() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: [],
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices: []});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "No Devices Connected", disabled: true },
     "----",
     { label: "Connect Another Device..." },
     { label: "Learn About Sending Tabs..." },
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_one_remote_client() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: [{ id: 1, name: "Foo"}],
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices: [{id: 1, name: "Foo", availableCommands: {"https://identity.mozilla.com/cmd/open-uri": "baz"}}]});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "Foo" },
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_not_sendable() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture,
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: false });
+  const sandbox = setupSendTabMocks({fxaDevices, isSendableURI: false});
 
   await openContentContextMenu("#moztext");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, true, "Send tab to device is disabled");
   checkPopup();
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_not_synced_yet() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: false, targets: [],
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices: null});
 
   await openContentContextMenu("#moztext");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, true, "Send tab to device is disabled");
   checkPopup();
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_sync_not_ready_configured() {
-  const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null,
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({syncReady: false});
 
   await openContentContextMenu("#moztext");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, true, "Send tab to device is disabled");
   checkPopup();
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_sync_not_ready_other_state() {
-  const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null,
-                                      state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true });
+  const sandbox = setupSendTabMocks({syncReady: false, state: UIState.STATUS_NOT_VERIFIED});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "Account Not Verified", disabled: true },
     "----",
     { label: "Verify Your Account..." },
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_unconfigured() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: null,
-                                      state: UIState.STATUS_NOT_CONFIGURED, isSendableURI: true });
+  const sandbox = setupSendTabMocks({state: UIState.STATUS_NOT_CONFIGURED});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "Not Connected to Sync", disabled: true },
     "----",
     { label: "Sign in to Sync..." },
@@ -162,52 +157,47 @@ add_task(async function test_page_contex
   ]);
 
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_not_verified() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: null,
-                                      state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true });
+  const sandbox = setupSendTabMocks({state: UIState.STATUS_NOT_VERIFIED});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "Account Not Verified", disabled: true },
     "----",
     { label: "Verify Your Account..." },
   ]);
 
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_login_failed() {
-  const syncReady = sinon.stub(gSync, "syncReady").get(() => true);
-  const getState = sinon.stub(UIState, "get").returns({ status: UIState.STATUS_LOGIN_FAILED });
-  const isSendableURI = sinon.stub(gSync, "isSendableURI").returns(true);
+  const sandbox = setupSendTabMocks({state: UIState.STATUS_LOGIN_FAILED});
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "Account Not Verified", disabled: true },
     "----",
     { label: "Verify Your Account..." },
   ]);
 
   await hideContentContextMenu();
 
-  syncReady.restore();
-  getState.restore();
-  isSendableURI.restore();
+  sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_fxa_disabled() {
   const getter = sinon.stub(gSync, "SYNC_ENABLED").get(() => false);
   gSync.onSyncDisabled(); // Would have been called on gSync initialization if SYNC_ENABLED had been set.
   await openContentContextMenu("#moztext");
   is(document.getElementById("context-sendpagetodevice").hidden, true, "Send tab to device is hidden");
   is(document.getElementById("context-sep-sendpagetodevice").hidden, true, "Separator is also hidden");
--- a/browser/base/content/test/sync/browser_contextmenu_sendtab.js
+++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
@@ -2,24 +2,26 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
 Services.scriptloader.loadSubScript(chrome_base + "head.js", this);
 /* import-globals-from ../general/head.js */
 
-const targetsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+const fxaDevices = [
+  {id: 1, name: "Foo", availableCommands: {"https://identity.mozilla.com/cmd/open-uri": "baz"}},
+  {id: 2, name: "Bar", clientRecord: "bar"}, // Legacy send tab target (no availableCommands).
+  {id: 3, name: "Homer"}, // Incompatible target.
+];
 
 let [testTab] = gBrowser.visibleTabs;
 
-function updateTabContextMenu(tab) {
+function updateTabContextMenu(tab = gBrowser.selectedTab) {
   let menu = document.getElementById("tabContextMenu");
-  if (!tab)
-    tab = gBrowser.selectedTab;
   var evt = new Event("");
   tab.dispatchEvent(evt);
   menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
   is(window.TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
   menu.hidePopup();
 }
 
 add_task(async function setup() {
@@ -30,82 +32,76 @@ add_task(async function setup() {
   await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
   registerCleanupFunction(() => {
     gBrowser.removeCurrentTab();
   });
   is(gBrowser.visibleTabs.length, 2, "there are two visible tabs");
 });
 
 add_task(async function test_tab_contextmenu() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture,
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices});
   let expectation = sandbox.mock(gSync)
                            .expects("sendTabToDevice")
                            .once()
-                           .withExactArgs("about:mozilla", [{id: 1, name: "Foo"}], "The Book of Mozilla, 11:14");
+                           .withExactArgs("about:mozilla", [fxaDevices[1]], "The Book of Mozilla, 11:14");
 
   updateTabContextMenu(testTab);
   await openTabContextMenu("context_sendTabToDevice");
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
 
   document.getElementById("context_sendTabToDevicePopupMenu").querySelector("menuitem").click();
 
   await hideTabContextMenu();
   expectation.verify();
   sandbox.restore();
 });
 
 add_task(async function test_tab_contextmenu_unconfigured() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: targetsFixture,
-                                      state: UIState.STATUS_NOT_CONFIGURED, isSendableURI: true });
+  const sandbox = setupSendTabMocks({state: UIState.STATUS_NOT_CONFIGURED});
 
   updateTabContextMenu(testTab);
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
 
   sandbox.restore();
 });
 
 add_task(async function test_tab_contextmenu_not_sendable() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, targets: [{ id: 1, name: "Foo"}],
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: false });
+  const sandbox = setupSendTabMocks({fxaDevices, isSendableURI: false});
 
   updateTabContextMenu(testTab);
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, true, "Send tab to device is disabled");
 
   sandbox.restore();
 });
 
 add_task(async function test_tab_contextmenu_not_synced_yet() {
-  const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: false, targets: [],
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({fxaDevices: null});
 
   updateTabContextMenu(testTab);
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, true, "Send tab to device is disabled");
 
   sandbox.restore();
 });
 
 add_task(async function test_tab_contextmenu_sync_not_ready_configured() {
-  const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null,
-                                      state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
+  const sandbox = setupSendTabMocks({syncReady: false});
 
   updateTabContextMenu(testTab);
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, true, "Send tab to device is disabled");
 
   sandbox.restore();
 });
 
 add_task(async function test_tab_contextmenu_sync_not_ready_other_state() {
-  const sandbox = setupSendTabMocks({ syncReady: false, clientsSynced: false, targets: null,
-                                      state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true });
+  const sandbox = setupSendTabMocks({syncReady: false, state: UIState.STATUS_NOT_VERIFIED});
 
   updateTabContextMenu(testTab);
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
 
   sandbox.restore();
 });
 
--- a/browser/base/content/test/sync/head.js
+++ b/browser/base/content/test/sync/head.js
@@ -9,17 +9,21 @@ registerCleanupFunction(function() {
 
 function promiseSyncReady() {
   let service = Cc["@mozilla.org/weave/service;1"]
                   .getService(Ci.nsISupports)
                   .wrappedJSObject;
   return service.whenLoaded();
 }
 
-function setupSendTabMocks({ syncReady, clientsSynced, targets, state, isSendableURI }) {
+function setupSendTabMocks({ syncReady = true, fxaDevices = null,
+                             state = UIState.STATUS_SIGNED_IN, isSendableURI = true }) {
   const sandbox = sinon.sandbox.create();
   sandbox.stub(gSync, "syncReady").get(() => syncReady);
-  sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => !clientsSynced);
-  sandbox.stub(gSync, "sendTabTargets").get(() => targets);
+  if (fxaDevices) {
+    // Clone fxaDevices because it gets sorted in-place.
+    sandbox.stub(Weave.Service.clientsEngine, "fxaDevices").get(() => [...fxaDevices]);
+  }
+  sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => !!fxaDevices);
   sandbox.stub(UIState, "get").returns({ status: state });
   sandbox.stub(gSync, "isSendableURI").returns(isSendableURI);
   return sandbox;
 }
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -200,17 +200,16 @@ add_task(async function sendToDevice_non
 });
 
 add_task(async function sendToDevice_syncNotReady_other_states() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
     const sandbox = sinon.sandbox.create();
     sandbox.stub(gSync, "syncReady").get(() => false);
-    sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => true);
     sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_NOT_VERIFIED });
     sandbox.stub(gSync, "isSendableURI").returns(true);
 
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
@@ -257,23 +256,23 @@ add_task(async function sendToDevice_syn
 });
 
 add_task(async function sendToDevice_syncNotReady_configured() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
     const sandbox = sinon.sandbox.create();
     const syncReady = sandbox.stub(gSync, "syncReady").get(() => false);
-    const lastSync = sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => true);
+    const hasSyncedThisSession = sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => false);
     sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
     sandbox.stub(gSync, "isSendableURI").returns(true);
 
     sandbox.stub(Weave.Service, "sync").callsFake(() => {
       syncReady.get(() => true);
-      lastSync.get(() => Date.now());
+      hasSyncedThisSession.get(() => true);
       sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets);
       sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);
     });
 
     let onShowingSubview = BrowserPageActions.sendToDevice.onShowingSubview;
     sandbox.stub(BrowserPageActions.sendToDevice, "onShowingSubview").callsFake((...args) => {
       this.numCall++ || (this.numCall = 1);
       onShowingSubview.call(BrowserPageActions.sendToDevice, ...args);
@@ -311,23 +310,26 @@ add_task(async function sendToDevice_syn
         let expectedItems = [
           {
             className: "pageAction-sendToDevice-notReady",
             display: "none",
             disabled: true,
           },
         ];
         for (let target of mockTargets) {
+          const attrs = {
+            clientId: target.id,
+            label: target.name,
+            clientType: target.type,
+          };
+          if (target.clientRecord && target.clientRecord.serverLastModified) {
+            attrs.tooltiptext = gSync.formatLastSyncDate(new Date(target.clientRecord.serverLastModified * 1000));
+          }
           expectedItems.push({
-            attrs: {
-              clientId: target.id,
-              label: target.name,
-              clientType: target.type,
-              tooltiptext: target.clientRecord ? gSync.formatLastSyncDate(new Date(lastModifiedFixture * 1000)) : "",
-            },
+            attrs,
           });
         }
         expectedItems.push(
           null,
           {
             attrs: {
               label: "Send to All Devices",
             },
@@ -398,20 +400,20 @@ add_task(async function sendToDevice_not
 });
 
 add_task(async function sendToDevice_noDevices() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
     const sandbox = sinon.sandbox.create();
     sandbox.stub(gSync, "syncReady").get(() => true);
-    sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false);
+    sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
+    sandbox.stub(Weave.Service.clientsEngine, "fxaDevices").get(() => []);
     sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
     sandbox.stub(gSync, "isSendableURI").returns(true);
-    sandbox.stub(gSync, "sendTabTargets").get(() => []);
     sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);
 
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Open the panel.
@@ -464,17 +466,17 @@ add_task(async function sendToDevice_noD
 });
 
 add_task(async function sendToDevice_devices() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
     const sandbox = sinon.sandbox.create();
     sandbox.stub(gSync, "syncReady").get(() => true);
-    sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false);
+    sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
     sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
     sandbox.stub(gSync, "isSendableURI").returns(true);
     sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets);
     sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);
 
     let cleanUp = () => {
       sandbox.restore();
     };
@@ -530,17 +532,17 @@ add_task(async function sendToDevice_dev
 
 add_task(async function sendToDevice_title() {
   // Open two tabs that are sendable.
   await BrowserTestUtils.withNewTab("http://example.com/a", async otherBrowser => {
     await BrowserTestUtils.withNewTab("http://example.com/b", async () => {
       await promiseSyncReady();
       const sandbox = sinon.sandbox.create();
       sandbox.stub(gSync, "syncReady").get(() => true);
-      sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false);
+      sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
       sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
       sandbox.stub(gSync, "isSendableURI").returns(true);
       sandbox.stub(gSync, "sendTabTargets").get(() => []);
       sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);
 
       let cleanUp = () => {
         sandbox.restore();
       };
@@ -587,17 +589,17 @@ add_task(async function sendToDevice_tit
 });
 
 add_task(async function sendToDevice_inUrlbar() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
     const sandbox = sinon.sandbox.create();
     sandbox.stub(gSync, "syncReady").get(() => true);
-    sandbox.stub(Weave.Service.clientsEngine, "isFirstSync").get(() => false);
+    sandbox.stub(Weave.Service.clientsEngine, "hasSyncedThisSession").get(() => true);
     sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
     sandbox.stub(gSync, "isSendableURI").returns(true);
     sandbox.stub(gSync, "sendTabTargets").get(() => mockTargets);
     sandbox.stub(Weave.Service.clientsEngine, "getClientType").callsFake(id => mockTargets.find(c => c.clientRecord && c.clientRecord.id == id).clientRecord.type);
 
     let cleanUp = () => {
       sandbox.restore();
     };