Bug 1368383 part 1 - Always show Send Tab to Device in the context menu. r=markh
☠☠ backed out by 807243861494 ☠ ☠
authorEdouard Oger <eoger@fastmail.com>
Thu, 01 Jun 2017 13:02:14 -0400
changeset 410967 3ca93081969bdb74ada3974ce36afa7dd327bbb2
parent 410966 9721edbfbba3c928d3dee5a5abe9b5c9bb12fb05
child 410968 4eb7778c4325c8ab17c4576aaf57e952cddbadbe
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1368383
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 1368383 part 1 - Always show Send Tab to Device in the context menu. r=markh MozReview-Commit-ID: 1C2aqQIpKAJ
browser/app/profile/firefox.js
browser/base/content/browser-sync.js
browser/base/content/browser.xul
browser/base/content/test/contextMenu/browser_contextmenu_mozextension.js
browser/base/content/test/general/browser_contextmenu.js
browser/base/content/test/general/browser_visibleTabs_contextMenu.js
browser/base/content/test/general/head.js
browser/base/content/test/sync/browser.ini
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
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1206,18 +1206,16 @@ pref("services.sync.prefs.sync.spellchec
 pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
 
 // A preference that controls whether we should show the icon for a remote tab.
 // This pref has no UI but exists because some people may be concerned that
 // fetching these icons to show remote tabs may leak information about that
 // user's tabs and bookmarks. Note this pref is also synced.
 pref("services.sync.syncedTabs.showRemoteIcons", true);
 
-pref("services.sync.sendTabToDevice.enabled", true);
-
 // Developer edition preferences
 #ifdef MOZ_DEV_EDITION
 sticky_pref("lightweightThemes.selectedThemeID", "firefox-compact-dark@mozilla.org");
 #else
 sticky_pref("lightweightThemes.selectedThemeID", "");
 #endif
 
 // Whether the character encoding menu is under the main Firefox button. This
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -35,18 +35,18 @@ var gSync = {
     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 syncReady() {
+    return Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject.ready;
   },
 
   get remoteClients() {
     return Weave.Service.clientsEngine.remoteClients
            .sort((a, b) => a.name.localeCompare(b.name));
   },
 
   _generateNodeGetters(usePhoton) {
@@ -351,54 +351,52 @@ var gSync = {
       // 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;
     }
   },
 
+  // "Send Tab to Device" menu item
   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 });
+    const enabled = this.syncReady &&
+                    this.remoteClients.length > 0 &&
+                    this.isSendableURI(aTargetTab.linkedBrowser.currentURI.spec);
+    document.getElementById("context_sendTabToDevice").disabled = !enabled;
   },
 
+  // "Send Page to Device" and "Send Link to Device" menu items
   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
+    const showSendLink = contextMenu.onSaveableLink || contextMenu.onPlainTextLink;
+    const showSendPage = !showSendLink
                          && !(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);
-    }
+                              contextMenu.onLink ||
+                              (contextMenu.target &&
+                               ["input", "textarea"].includes(contextMenu.target.tagName.toLowerCase())));
 
     ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
     .forEach(id => contextMenu.showItem(id, showSendPage));
     ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
     .forEach(id => contextMenu.showItem(id, showSendLink));
+
+    if (!showSendLink && !showSendPage) {
+      return;
+    }
+
+    const targetURI = showSendLink ? contextMenu.linkURL :
+                                     contextMenu.browser.currentURI.spec;
+    const enabled = this.syncReady && this.remoteClients.length > 0 &&
+                    this.isSendableURI(targetURI);
+    contextMenu.setItemAttr(showSendPage ? "context-sendpagetodevice" :
+                                           "context-sendlinktodevice",
+                            "disabled", !enabled || null);
   },
 
   // Functions called by observers
   onActivityStart() {
     clearTimeout(this._syncAnimationTimer);
     this._syncStartTime = Date.now();
 
     let broadcaster = document.getElementById("sync-status");
@@ -562,14 +560,8 @@ var gSync = {
     }
   },
 
   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;
-});
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -97,19 +97,19 @@
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
 #ifdef E10S_TESTING_ONLY
       <menuitem id="context_openNonRemoteWindow" label="Open in new non-e10s window"
                 tbattr="tabbrowser-remote"
                 hidden="true"
                 oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
 #endif
-      <menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
+      <menuseparator id="context_sendTabToDevice_separator"/>
       <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
-            accesskey="&sendTabToDevice.accesskey;" hidden="true">
+            accesskey="&sendTabToDevice.accesskey;">
         <menupopup id="context_sendTabToDevicePopupMenu"
                    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"
--- a/browser/base/content/test/contextMenu/browser_contextmenu_mozextension.js
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_mozextension.js
@@ -39,17 +39,19 @@ add_task(async function test_link() {
        // We need a blank entry here because the containers submenu is
        // dynamically generated with no ids.
        ...(hasContainers ? ["", null] : []),
        "context-openlink",      true,
        "context-openlinkprivate", true,
        "---",                   null,
        "context-savelink",      true,
        "context-copylink",      true,
-       "context-searchselect",  true]);
+       "context-searchselect",  true,
+       "---", null,
+       "context-sendlinktodevice", false, [], null]);
 });
 
 add_task(async function test_video() {
   await test_contextmenu("#video",
   ["context-media-play",         null,
    "context-media-mute",         null,
    "context-media-playbackrate", null,
        ["context-media-playbackrate-050x", null,
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -33,17 +33,19 @@ add_task(async function test_xul_text_li
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
      "context-openlinkprivate", true,
      "---",                   null,
      "context-bookmarklink",  true,
      "context-savelink",      true,
      ...(hasPocket ? ["context-savelinktopocket", true] : []),
      "context-copylink",      true,
-     "context-searchselect",  true
+     "context-searchselect",  true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
     ]
   );
 
   // Clean up so won't affect HTML element test cases
   lastElementSelector = null;
   gBrowser.removeCurrentTab();
 });
 
@@ -84,16 +86,18 @@ add_task(async function test_plaintext()
   plainTextItems = ["context-navigation",   null,
                         ["context-back",         false,
                          "context-forward",      false,
                          "context-reload",       true,
                          "context-bookmarkpage", true], null,
                     "---",                  null,
                     "context-savepage",     true,
                     ...(hasPocket ? ["context-pocket", true] : []),
+                    "---", null,
+                    "context-sendpagetodevice", false, [], null,
                     "---",                  null,
                     "context-viewbgimage",  false,
                     "context-selectall",    true,
                     "---",                  null,
                     "context-viewsource",   true,
                     "context-viewinfo",     true
                    ];
   await test_contextmenu("#test-text", plainTextItems, {
@@ -110,17 +114,19 @@ add_task(async function test_link() {
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
      "context-openlinkprivate", true,
      "---",                   null,
      "context-bookmarklink",  true,
      "context-savelink",      true,
      ...(hasPocket ? ["context-savelinktopocket", true] : []),
      "context-copylink",      true,
-     "context-searchselect",  true
+     "context-searchselect",  true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
     ]
   );
 });
 
 add_task(async function test_mailto() {
   await test_contextmenu("#test-mailto",
     ["context-copyemail", true,
      "context-searchselect", true
@@ -257,16 +263,18 @@ add_task(async function test_iframe() {
     ["context-navigation", null,
          ["context-back",         false,
           "context-forward",      false,
           "context-reload",       true,
           "context-bookmarkpage", true], null,
      "---",                  null,
      "context-savepage",     true,
      ...(hasPocket ? ["context-pocket", true] : []),
+     "---", null,
+     "context-sendpagetodevice", false, [], null,
      "---",                  null,
      "context-viewbgimage",  false,
      "context-selectall",    true,
      "frame",                null,
          ["context-showonlythisframe", true,
           "context-openframeintab",    true,
           "context-openframe",         true,
           "---",                       null,
@@ -561,16 +569,18 @@ add_task(async function test_pagemenu() 
          ["+Radio1",             {type: "checkbox", icon: "", checked: false, disabled: false},
           "+Radio2",             {type: "checkbox", icon: "", checked: true, disabled: false},
           "+Radio3",             {type: "checkbox", icon: "", checked: false, disabled: false},
           "---",                 null,
           "+Checkbox",           {type: "checkbox", icon: "", checked: false, disabled: false}], null,
      "---",                  null,
      "context-savepage",     true,
      ...(hasPocket ? ["context-pocket", true] : []),
+     "---", null,
+     "context-sendpagetodevice", false, [], null,
      "---",                  null,
      "context-viewbgimage",  false,
      "context-selectall",    true,
      "---",                  null,
      "context-viewsource",   true,
      "context-viewinfo",     true
     ],
     {async postCheckContextMenuFn() {
@@ -593,16 +603,18 @@ add_task(async function test_dom_full_sc
           "context-forward",         false,
           "context-reload",          true,
           "context-bookmarkpage",    true], null,
      "---",                          null,
      "context-leave-dom-fullscreen", true,
      "---",                          null,
      "context-savepage",             true,
      ...(hasPocket ? ["context-pocket", true] : []),
+     "---", null,
+     "context-sendpagetodevice", false, [], null,
      "---",                          null,
      "context-viewbgimage",          false,
      "context-selectall",            true,
      "---",                          null,
      "context-viewsource",           true,
      "context-viewinfo",             true
     ],
     {
@@ -640,16 +652,18 @@ add_task(async function test_pagemenu2()
     ["context-navigation", null,
          ["context-back",         false,
           "context-forward",      false,
           "context-reload",       true,
           "context-bookmarkpage", true], null,
      "---",                  null,
      "context-savepage",     true,
      ...(hasPocket ? ["context-pocket", true] : []),
+     "---", null,
+     "context-sendpagetodevice", false, [], null,
      "---",                  null,
      "context-viewbgimage",  false,
      "context-selectall",    true,
      "---",                  null,
      "context-viewsource",   true,
      "context-viewinfo",     true
     ],
     {maybeScreenshotsPresent: true,
@@ -687,16 +701,18 @@ add_task(async function test_select_text
      "context-openlinkprivate",             true,
      "---",                                 null,
      "context-bookmarklink",                true,
      "context-savelink",                    true,
      "context-copy",                        true,
      "context-selectall",                   true,
      "---",                                 null,
      "context-searchselect",                true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
      "context-viewpartialsource-selection", true
     ],
     {
       offsetX: 6,
       offsetY: 6,
       async preCheckContextMenuFn() {
         await selectText("#test-select-text-link");
       },
@@ -727,17 +743,19 @@ add_task(async function test_imagelink()
      "---",                   null,
      "context-viewimage",            true,
      "context-copyimage-contents",   true,
      "context-copyimage",            true,
      "---",                          null,
      "context-saveimage",            true,
      "context-sendimage",            true,
      "context-setDesktopBackground", true,
-     "context-viewimageinfo",        true
+     "context-viewimageinfo",        true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
     ]
   );
 });
 
 add_task(async function test_select_input_text() {
   todo(false, "spell checker tests are failing, bug 1246296");
 
   /*
@@ -819,16 +837,18 @@ add_task(async function test_click_to_pl
           "context-reload",       true,
           "context-bookmarkpage", true], null,
      "---",                  null,
      "context-ctp-play",     true,
      "context-ctp-hide",     true,
      "---",                  null,
      "context-savepage",     true,
      ...(hasPocket ? ["context-pocket", true] : []),
+     "---", null,
+     "context-sendpagetodevice", false, [], null,
      "---",                  null,
      "context-viewbgimage",  false,
      "context-selectall",    true,
      "---",                  null,
      "context-viewsource",   true,
      "context-viewinfo",     true
     ],
     {
@@ -864,16 +884,18 @@ add_task(async function test_srcdoc() {
     ["context-navigation", null,
          ["context-back",         false,
           "context-forward",      false,
           "context-reload",       true,
           "context-bookmarkpage", true], null,
      "---",                  null,
      "context-savepage",     true,
      ...(hasPocket ? ["context-pocket", true] : []),
+     "---", null,
+     "context-sendpagetodevice", false, [], null,
      "---",                  null,
      "context-viewbgimage",  false,
      "context-selectall",    true,
      "frame",                null,
          ["context-reloadframe",       true,
           "---",                       null,
           "context-saveframe",         true,
           "---",                       null,
@@ -901,143 +923,71 @@ add_task(async function test_input_spell
      "context-delete",      false,
      "---",                 null,
      "context-selectall",   true,
     ]
   );
   */
 });
 
-const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
-
-add_task(async function test_plaintext_sendpagetodevice() {
-  if (!gSync.sendTabToDeviceEnabled) {
-    return;
-  }
-  await ensureSyncReady();
-  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
-
-  let plainTextItemsWithSendPage =
-                    ["context-navigation",   null,
-                      ["context-back",         false,
-                        "context-forward",      false,
-                        "context-reload",       true,
-                        "context-bookmarkpage", true], null,
-                    "---",                  null,
-                    "context-savepage",     true,
-                    ...(hasPocket ? ["context-pocket", true] : []),
-                    "---",                  null,
-                    "context-sendpagetodevice", true,
-                      ["*Foo", true,
-                       "*Bar", true,
-                       "---", null,
-                       "*All Devices", true], null,
-                    "---",                  null,
-                    "context-viewbgimage",  false,
-                    "context-selectall",    true,
-                    "---",                  null,
-                    "context-viewsource",   true,
-                    "context-viewinfo",     true
-                   ];
-  await test_contextmenu("#test-text", plainTextItemsWithSendPage, {
-      maybeScreenshotsPresent: true,
-      async onContextMenuShown() {
-        await openMenuItemSubmenu("context-sendpagetodevice");
-      }
-    });
-
-  restoreRemoteClients(oldGetter);
-});
-
-add_task(async function test_link_sendlinktodevice() {
-  if (!gSync.sendTabToDeviceEnabled) {
-    return;
-  }
-  await ensureSyncReady();
-  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
-
-  await test_contextmenu("#test-link",
-    ["context-openlinkintab", true,
-     ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
-     // We need a blank entry here because the containers submenu is
-     // dynamically generated with no ids.
-     ...(hasContainers ? ["", null] : []),
-     "context-openlink",      true,
-     "context-openlinkprivate", true,
-     "---",                   null,
-     "context-bookmarklink",  true,
-     "context-savelink",      true,
-     ...(hasPocket ? ["context-savelinktopocket", true] : []),
-     "context-copylink",      true,
-     "context-searchselect",  true,
-     "---",                  null,
-     "context-sendlinktodevice", true,
-      ["*Foo", true,
-       "*Bar", true,
-       "---", null,
-       "*All Devices", true], null,
-    ],
-    {
-      async onContextMenuShown() {
-        await openMenuItemSubmenu("context-sendlinktodevice");
-      }
-    });
-
-  restoreRemoteClients(oldGetter);
-});
-
 add_task(async function test_svg_link() {
   await test_contextmenu("#svg-with-link > a",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
      // We need a blank entry here because the containers submenu is
      // dynamically generated with no ids.
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
      "context-openlinkprivate", true,
      "---",                   null,
      "context-bookmarklink",  true,
      "context-savelink",      true,
      ...(hasPocket ? ["context-savelinktopocket", true] : []),
      "context-copylink",      true,
-     "context-searchselect",  true
+     "context-searchselect",  true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
     ]
   );
 
   await test_contextmenu("#svg-with-link2 > a",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
      // We need a blank entry here because the containers submenu is
      // dynamically generated with no ids.
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
      "context-openlinkprivate", true,
      "---",                   null,
      "context-bookmarklink",  true,
      "context-savelink",      true,
      ...(hasPocket ? ["context-savelinktopocket", true] : []),
      "context-copylink",      true,
-     "context-searchselect",  true
+     "context-searchselect",  true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
     ]
   );
 
   await test_contextmenu("#svg-with-link3 > a",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
      // We need a blank entry here because the containers submenu is
      // dynamically generated with no ids.
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
      "context-openlinkprivate", true,
      "---",                   null,
      "context-bookmarklink",  true,
      "context-savelink",      true,
      ...(hasPocket ? ["context-savelinktopocket", true] : []),
      "context-copylink",      true,
-     "context-searchselect",  true
+     "context-searchselect",  true,
+     "---", null,
+     "context-sendlinktodevice", false, [], null,
     ]
   );
 });
 
 add_task(async function test_cleanup_html() {
   gBrowser.removeCurrentTab();
 });
 
@@ -1057,15 +1007,8 @@ async function selectText(selector) {
     let div = doc.createRange();
     let element = doc.querySelector(contentSelector);
     Assert.ok(element, "Found element to select text from");
     div.setStartBefore(element);
     div.setEndAfter(element);
     win.getSelection().addRange(div);
   });
 }
-
-function ensureSyncReady() {
-  let service = Cc["@mozilla.org/weave/service;1"]
-                  .getService(Components.interfaces.nsISupports)
-                  .wrappedJSObject;
-  return service.whenLoaded();
-}
--- a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -11,38 +11,16 @@ add_task(async function test() {
   let testTab = BrowserTestUtils.addTab(gBrowser);
   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 (gSync.sendTabToDeviceEnabled) {
-    const origIsSendableURI = gSync.isSendableURI;
-    gSync.isSendableURI = () => true;
-    // Check the send tab to device menu item
-    await ensureSyncReady();
-    const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
-    await updateTabContextMenu(origTab, async function() {
-      await 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");
-    gSync.isSendableURI = () => false;
-    updateTabContextMenu(origTab);
-    is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
-    restoreRemoteClients(oldGetter);
-    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.
   updateTabContextMenu(testTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled when more than one tab exists");
@@ -72,15 +50,8 @@ add_task(async function test() {
   // Close Tabs To The End should now be enabled
   updateTabContextMenu(origTab);
   is(document.getElementById("context_closeTabsToTheEnd").disabled, false, "Close Tabs To The End is enabled");
 
   gBrowser.removeTab(testTab);
   gBrowser.removeTab(pinned);
 });
 
-function ensureSyncReady() {
-  let service = Cc["@mozilla.org/weave/service;1"]
-                  .getService(Components.interfaces.nsISupports)
-                  .wrappedJSObject;
-  return service.whenLoaded();
-}
-
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -826,31 +826,8 @@ function getCertExceptionDialog(aLocatio
 
       if (childDoc.location.href == aLocation) {
         return childDoc;
       }
     }
   }
   return undefined;
 }
-
-function setupRemoteClientsFixture(fixture) {
-  let oldRemoteClientsGetter =
-    Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
-
-  Object.defineProperty(gSync, "remoteClients", {
-    get() { return fixture; }
-  });
-  return oldRemoteClientsGetter;
-}
-
-function restoreRemoteClients(getter) {
-  Object.defineProperty(gSync, "remoteClients", {
-    get: getter
-  });
-}
-
-async function openMenuItemSubmenu(id) {
-  let menuPopup = document.getElementById(id).menupopup;
-  let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
-  menuPopup.showPopup();
-  await menuPopupPromise;
-}
--- a/browser/base/content/test/sync/browser.ini
+++ b/browser/base/content/test/sync/browser.ini
@@ -1,9 +1,15 @@
+[DEFAULT]
+support-files =
+  head.js
+
 [browser_sync.js]
+[browser_contextmenu_sendtab.js]
+[browser_contextmenu_sendpage.js]
 [browser_fxa_web_channel.js]
 support-files=
   browser_fxa_web_channel.html
 [browser_fxa_badge.js]
 [browser_aboutAccounts.js]
 skip-if = os == "linux" # Bug 958026
 support-files =
   content_aboutAccounts.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+const origRemoteClients = mockReturn(gSync, "remoteClients", remoteClientsFixture);
+const origSyncReady = mockReturn(gSync, "syncReady", true);
+const origIsSendableURI = mockReturn(gSync, "isSendableURI", true);
+
+add_task(async function setup() {
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+});
+
+add_task(async function test_page_contextmenu() {
+  await updateContentContextMenu("#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");
+  let devices = document.getElementById("context-sendpagetodevice-popup").childNodes;
+  is(devices[0].getAttribute("label"), "Foo", "Foo target is present");
+  is(devices[1].getAttribute("label"), "Bar", "Bar target is present");
+  is(devices[3].getAttribute("label"), "All Devices", "All Devices target is present");
+});
+
+add_task(async function test_page_contextmenu_notsendable() {
+  const isSendableURIMock = mockReturn(gSync, "isSendableURI", false);
+
+  await updateContentContextMenu("#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");
+
+  isSendableURIMock.restore();
+});
+
+add_task(async function test_page_contextmenu_sendtab_no_remote_clients() {
+  let remoteClientsMock = mockReturn(gSync, "remoteClients", []);
+
+  await updateContentContextMenu("#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");
+
+  remoteClientsMock.restore();
+});
+
+add_task(async function test_page_contextmenu_sync_not_ready() {
+  const syncReadyMock = mockReturn(gSync, "syncReady", false);
+
+  await updateContentContextMenu("#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");
+
+  syncReadyMock.restore();
+});
+
+// We are not going to bother testing the states of context-sendlinktodevice since they use
+// the exact same code.
+// However, browser_contextmenu.js contains tests that verify the menu item is present.
+
+add_task(async function cleanup() {
+  gBrowser.removeCurrentTab();
+  origSyncReady.restore();
+  origRemoteClients.restore();
+  origIsSendableURI.restore();
+});
+
+async function updateContentContextMenu(selector, openSubmenuId = null) {
+  let contextMenu = document.getElementById("contentAreaContextMenu");
+  is(contextMenu.state, "closed", "checking if popup is closed");
+
+  let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  await BrowserTestUtils.synthesizeMouse(selector, 0, 0, {
+      type: "contextmenu",
+      button: 2,
+      shiftkey: false,
+      centered: true
+    },
+    gBrowser.selectedBrowser);
+  await awaitPopupShown;
+
+  if (openSubmenuId) {
+    let menuPopup = document.getElementById(openSubmenuId).menupopup;
+    let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+    menuPopup.showPopup();
+    await menuPopupPromise;
+  }
+
+  let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+
+  contextMenu.hidePopup();
+  await awaitPopupHidden;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   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 remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+const origRemoteClients = mockReturn(gSync, "remoteClients", remoteClientsFixture);
+const origSyncReady = mockReturn(gSync, "syncReady", true);
+const origIsSendableURI = mockReturn(gSync, "isSendableURI", true);
+let [testTab] = gBrowser.visibleTabs;
+
+add_task(async function setup() {
+  is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
+});
+
+add_task(async function test_tab_contextmenu() {
+  await updateTabContextMenu(testTab, openSendTabTargetsSubmenu);
+  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");
+  let devices = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+  is(devices[0].getAttribute("label"), "Foo", "Foo target is present");
+  is(devices[1].getAttribute("label"), "Bar", "Bar target is present");
+  is(devices[3].getAttribute("label"), "All Devices", "All Devices target is present");
+});
+
+add_task(async function test_tab_contextmenu_only_one_remote_device() {
+  const remoteClientsMock = mockReturn(gSync, "remoteClients", [{ id: 1, name: "Foo"}]);
+
+  await updateTabContextMenu(testTab, openSendTabTargetsSubmenu);
+  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");
+  let devices = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+  is(devices.length, 1, "There should not be any separator or All Devices item");
+  is(devices[0].getAttribute("label"), "Foo", "Foo target is present");
+
+  remoteClientsMock.restore();
+});
+
+add_task(async function test_tab_contextmenu_not_sendable() {
+  const isSendableURIMock = mockReturn(gSync, "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");
+
+  isSendableURIMock.restore();
+});
+
+add_task(async function test_tab_contextmenu_no_remote_clients() {
+  let remoteClientsMock = mockReturn(gSync, "remoteClients", []);
+
+  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");
+
+  remoteClientsMock.restore();
+});
+
+add_task(async function test_tab_contextmenu_sync_not_ready() {
+  const syncReadyMock = mockReturn(gSync, "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");
+
+  syncReadyMock.restore();
+});
+
+add_task(async function cleanup() {
+  origSyncReady.restore();
+  origRemoteClients.restore();
+  origIsSendableURI.restore();
+});
+
+async function openSendTabTargetsSubmenu() {
+  let menuPopup = document.getElementById("context_sendTabToDevice").menupopup;
+  let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+  menuPopup.showPopup();
+  await menuPopupPromise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/head.js
@@ -0,0 +1,24 @@
+// Mocks a getter or a function
+// This is basically sinon.js (our in-tree version doesn't do getters :/) (see bug 1369855)
+function mockReturn(obj, symbol, fixture) {
+  let getter = Object.getOwnPropertyDescriptor(obj, symbol).get;
+  if (getter) {
+    Object.defineProperty(obj, symbol, {
+      get() { return fixture; }
+    });
+    return {
+      restore() {
+        Object.defineProperty(obj, symbol, {
+          get: getter
+        });
+      }
+    }
+  }
+  let func = obj[symbol];
+  obj[symbol] = () => fixture;
+  return {
+    restore() {
+      obj[symbol] = func;
+    }
+  }
+}