--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -304,16 +304,18 @@ const CustomizableWidgets = [
viewId: "PanelUI-remotetabs",
defaultArea: CustomizableUI.AREA_PANEL,
deckIndices: {
DECKINDEX_TABS: 0,
DECKINDEX_TABSDISABLED: 1,
DECKINDEX_FETCHING: 2,
DECKINDEX_NOCLIENTS: 3,
},
+ TABS_PER_PAGE: 25,
+ NEXT_PAGE_MIN_TABS: 5, // Minimum number of tabs displayed when we click "Show All"
onCreated(aNode) {
// Add an observer to the button so we get the animation during sync.
// (Note the observer sets many attributes, including label and
// tooltiptext, but we only want the 'syncstatus' attribute for the
// animation)
let doc = aNode.ownerDocument;
let obnode = doc.createElementNS(kNSXUL, "observes");
obnode.setAttribute("element", "sync-status");
@@ -396,23 +398,23 @@ const CustomizableWidgets = [
// We call setAttribute instead of relying on the XBL property setter due
// to things going wrong when we try and set the index before the XBL
// binding has been created - see bug 1241851 for the gory details.
deck.setAttribute("selectedIndex", index);
},
_showTabsPromise: Promise.resolve(),
// Update the tab list after any existing in-flight updates are complete.
- _showTabs() {
+ _showTabs(paginationInfo) {
this._showTabsPromise = this._showTabsPromise.then(() => {
- return this.__showTabs();
+ return this.__showTabs(paginationInfo);
});
},
// Return a new promise to update the tab list.
- __showTabs() {
+ __showTabs(paginationInfo) {
let doc = this._tabsList.ownerDocument;
return SyncedTabs.getTabClients().then(clients => {
// The view may have been hidden while the promise was resolving.
if (!this._tabsList) {
return;
}
if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
// the "fetching tabs" deck is being shown - let's leave it there.
@@ -422,26 +424,30 @@ const CustomizableWidgets = [
if (clients.length === 0) {
this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS);
return;
}
this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
this._clearTabList();
- SyncedTabs.sortTabClientsByLastUsed(clients, 50 /* maxTabs */);
+ SyncedTabs.sortTabClientsByLastUsed(clients);
let fragment = doc.createDocumentFragment();
for (let client of clients) {
// add a menu separator for all clients other than the first.
if (fragment.lastChild) {
let separator = doc.createElementNS(kNSXUL, "menuseparator");
fragment.appendChild(separator);
}
- this._appendClient(client, fragment);
+ if (paginationInfo && paginationInfo.clientId == client.id) {
+ this._appendClient(client, fragment, paginationInfo.maxTabs);
+ } else {
+ this._appendClient(client, fragment);
+ }
}
this._tabsList.appendChild(fragment);
}).catch(err => {
Cu.reportError(err);
}).then(() => {
// an observer for tests.
Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated", null);
});
@@ -461,36 +467,56 @@ const CustomizableWidgets = [
}
let message = this._tabsList.getAttribute(messageAttr);
let doc = this._tabsList.ownerDocument;
let messageLabel = doc.createElementNS(kNSXUL, "label");
messageLabel.textContent = message;
appendTo.appendChild(messageLabel);
return messageLabel;
},
- _appendClient(client, attachFragment) {
+ _appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
let doc = attachFragment.ownerDocument;
// Create the element for the remote client.
let clientItem = doc.createElementNS(kNSXUL, "label");
clientItem.setAttribute("itemtype", "client");
let window = doc.defaultView;
clientItem.setAttribute("tooltiptext",
window.gSyncUI.formatLastSyncDate(new Date(client.lastModified)));
clientItem.textContent = client.name;
attachFragment.appendChild(clientItem);
if (client.tabs.length == 0) {
let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
} else {
+ // If this page will display all tabs, show no additional buttons.
+ // If the next page will display all the remaining tabs, show a "Show All" button
+ // Otherwise, show a "Shore More" button
+ let hasNextPage = client.tabs.length > maxTabs;
+ let nextPageIsLastPage = hasNextPage && maxTabs + this.TABS_PER_PAGE >= client.tabs.length;
+ if (nextPageIsLastPage) {
+ // When the user clicks "Show All", try to have at least NEXT_PAGE_MIN_TABS more tabs
+ // to display in order to avoid user frustration
+ maxTabs = Math.min(client.tabs.length - this.NEXT_PAGE_MIN_TABS, maxTabs);
+ }
+ if (hasNextPage) {
+ client.tabs = client.tabs.slice(0, maxTabs);
+ }
for (let tab of client.tabs) {
let tabEnt = this._createTabElement(doc, tab);
attachFragment.appendChild(tabEnt);
}
+ if (hasNextPage) {
+ let showAllEnt = this._createShowMoreElement(doc, client.id,
+ nextPageIsLastPage ?
+ Infinity :
+ maxTabs + this.TABS_PER_PAGE);
+ attachFragment.appendChild(showAllEnt);
+ }
}
},
_createTabElement(doc, tabInfo) {
let item = doc.createElementNS(kNSXUL, "toolbarbutton");
let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
item.setAttribute("itemtype", "tab");
item.setAttribute("class", "subviewbutton");
item.setAttribute("targetURI", tabInfo.url);
@@ -501,16 +527,39 @@ const CustomizableWidgets = [
// respects different buttons (eg, to open in a new tab).
item.addEventListener("click", e => {
doc.defaultView.openUILink(tabInfo.url, e);
CustomizableUI.hidePanelForNode(item);
BrowserUITelemetry.countSyncedTabEvent("open", "toolbarbutton-subview");
});
return item;
},
+ _createShowMoreElement(doc, clientId, showCount) {
+ let labelAttr, tooltipAttr;
+ if (showCount === Infinity) {
+ labelAttr = "showAllLabel";
+ tooltipAttr = "showAllTooltipText";
+ } else {
+ labelAttr = "showMoreLabel";
+ tooltipAttr = "showMoreTooltipText";
+ }
+ let showAllItem = doc.createElementNS(kNSXUL, "toolbarbutton");
+ showAllItem.setAttribute("itemtype", "showmorebutton");
+ showAllItem.setAttribute("class", "subviewbutton");
+ let label = this._tabsList.getAttribute(labelAttr);
+ showAllItem.setAttribute("label", label);
+ let tooltipText = this._tabsList.getAttribute(tooltipAttr);
+ showAllItem.setAttribute("tooltiptext", tooltipText);
+ showAllItem.addEventListener("click", e => {
+ e.preventDefault();
+ e.stopPropagation();
+ this._showTabs({ clientId, maxTabs: showCount });
+ });
+ return showAllItem;
+ }
}, {
id: "privatebrowsing-button",
shortcutId: "key_privatebrowsing",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand(e) {
let win = e.target.ownerGlobal;
win.OpenBrowserWindow({private: true});
}
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -120,16 +120,20 @@
oncommand="gSyncUI.doSync();"
closemenu="none"/>
<menuseparator id="PanelUI-remotetabs-separator"/>
</vbox>
<deck id="PanelUI-remotetabs-deck">
<!-- Sync is ready to Sync and the "tabs" engine is enabled -->
<vbox id="PanelUI-remotetabs-tabspane">
<vbox id="PanelUI-remotetabs-tabslist"
+ showAllLabel="&appMenuRemoteTabs.showAll.label;"
+ showAllTooltipText="&appMenuRemoteTabs.showAll.tooltip;"
+ showMoreLabel="&appMenuRemoteTabs.showMore.label;"
+ showMoreTooltipText="&appMenuRemoteTabs.showMore.tooltip;"
notabsforclientlabel="&appMenuRemoteTabs.notabs.label;"
/>
</vbox>
<!-- Sync is ready to Sync but the "tabs" engine isn't enabled-->
<hbox id="PanelUI-remotetabs-tabsdisabledpane" pack="center" flex="1">
<vbox class="PanelUI-remotetabs-instruction-box">
<hbox pack="center">
<html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -95,17 +95,16 @@ skip-if = os == "linux" # Intermittent f
[browser_948985_non_removable_defaultArea.js]
[browser_952963_areaType_getter_no_area.js]
[browser_956602_remove_special_widget.js]
[browser_962069_drag_to_overflow_chevron.js]
[browser_962884_opt_in_disable_hyphens.js]
[browser_963639_customizing_attribute_non_customizable_toolbar.js]
[browser_967000_button_charEncoding.js]
[browser_967000_button_feeds.js]
-[browser_967000_button_sync.js]
[browser_968447_bookmarks_toolbar_items_in_panel.js]
skip-if = os == "linux" # Intemittent failures - bug 979207
[browser_968565_insert_before_hidden_items.js]
[browser_969427_recreate_destroyed_widget_after_reset.js]
[browser_969661_character_encoding_navbar_disabled.js]
[browser_970511_undo_restore_default.js]
[browser_972267_customizationchange_events.js]
[browser_973641_button_addon.js]
@@ -146,9 +145,10 @@ skip-if = os == "mac"
[browser_1087303_button_preferences.js]
[browser_1089591_still_customizable_after_reset.js]
[browser_1096763_seen_widgets_post_reset.js]
[browser_1161838_inserted_new_default_buttons.js]
[browser_bootstrapped_custom_toolbar.js]
[browser_customizemode_contextmenu_menubuttonstate.js]
[browser_panel_toggle.js]
[browser_switch_to_customize_mode.js]
+[browser_synced_tabs_menu.js]
[browser_check_tooltips_in_navbar.js]
deleted file mode 100644
--- a/browser/components/customizableui/test/browser_967000_button_sync.js
+++ /dev/null
@@ -1,337 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-requestLongerTimeout(2);
-
-let {SyncedTabs} = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
-
-XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
-
-// These are available on the widget implementation, but it seems impossible
-// to grab that impl at runtime.
-const DECKINDEX_TABS = 0;
-const DECKINDEX_TABSDISABLED = 1;
-const DECKINDEX_FETCHING = 2;
-const DECKINDEX_NOCLIENTS = 3;
-
-var initialLocation = gBrowser.currentURI.spec;
-var newTab = null;
-
-// A helper to notify there are new tabs. Returns a promise that is resolved
-// once the UI has been updated.
-function updateTabsPanel() {
- let promiseTabsUpdated = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
- Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED, null);
- return promiseTabsUpdated;
-}
-
-// This is the mock we use for SyncedTabs.jsm - tests may override various
-// functions.
-let mockedInternal = {
- get isConfiguredToSyncTabs() { return true; },
- getTabClients() { return []; },
- syncTabs() {},
- hasSyncedThisSession: false,
-};
-
-
-add_task(function* setup() {
- let oldInternal = SyncedTabs._internal;
- SyncedTabs._internal = mockedInternal;
-
- // This test hacks some observer states to simulate a user being signed
- // in to Sync - restore them when the test completes.
- let initialObserverStates = {};
- for (let id of ["sync-reauth-state", "sync-setup-state", "sync-syncnow-state"]) {
- initialObserverStates[id] = document.getElementById(id).hidden;
- }
-
- registerCleanupFunction(() => {
- SyncedTabs._internal = oldInternal;
- for (let [id, initial] of Object.entries(initialObserverStates)) {
- document.getElementById(id).hidden = initial;
- }
- });
-});
-
-// The test expects the about:preferences#sync page to open in the current tab
-function* openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
- info("Check Sync button functionality");
- Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", "http://example.com/");
-
- // check the button's functionality
- yield PanelUI.show();
-
- if (entryPoint == "uitour") {
- UITour.tourBrowsersByWindow.set(window, new Set());
- UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
- }
-
- let syncButton = document.getElementById("sync-button");
- ok(syncButton, "The Sync button was added to the Panel Menu");
-
- syncButton.click();
- let syncPanel = document.getElementById("PanelUI-remotetabs");
- ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
-
- // Sync is not configured - verify that state is reflected.
- let subpanel = document.getElementById(expectedPanelId)
- ok(!subpanel.hidden, "sync setup element is visible");
-
- // Find and click the "setup" button.
- let setupButton = subpanel.querySelector(".PanelUI-remotetabs-prefs-button");
- setupButton.click();
-
- let deferred = Promise.defer();
- let handler = (e) => {
- if (e.originalTarget != gBrowser.selectedBrowser.contentDocument ||
- e.target.location.href == "about:blank") {
- info("Skipping spurious 'load' event for " + e.target.location.href);
- return;
- }
- gBrowser.selectedBrowser.removeEventListener("load", handler, true);
- deferred.resolve();
- }
- gBrowser.selectedBrowser.addEventListener("load", handler, true);
-
- yield deferred.promise;
- newTab = gBrowser.selectedTab;
-
- is(gBrowser.currentURI.spec, "about:preferences?entrypoint=" + entryPoint + "#sync",
- "Firefox Sync preference page opened with `menupanel` entrypoint");
- ok(!isPanelUIOpen(), "The panel closed");
-
- if (isPanelUIOpen()) {
- let panelHidePromise = promisePanelHidden(window);
- PanelUI.hide();
- yield panelHidePromise;
- }
-}
-
-function* asyncCleanup() {
- Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
- // reset the panel UI to the default state
- yield resetCustomization();
- ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
-
- // restore the tabs
- gBrowser.addTab(initialLocation);
- gBrowser.removeTab(newTab);
- UITour.tourBrowsersByWindow.delete(window);
-}
-
-// When Sync is not setup.
-add_task(() => openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs"));
-add_task(asyncCleanup);
-
-// When Sync is configured in a "needs reauthentication" state.
-add_task(function* () {
- // configure our broadcasters so we are in the right state.
- document.getElementById("sync-reauth-state").hidden = false;
- document.getElementById("sync-setup-state").hidden = true;
- document.getElementById("sync-syncnow-state").hidden = true;
- yield openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs")
-});
-
-// Test the mobile promo links
-add_task(function* () {
- // change the preferences for the mobile links.
- Services.prefs.setCharPref("identity.mobilepromo.android", "http://example.com/?os=android&tail=");
- Services.prefs.setCharPref("identity.mobilepromo.ios", "http://example.com/?os=ios&tail=");
-
- mockedInternal.getTabClients = () => [];
- mockedInternal.syncTabs = () => Promise.resolve();
-
- document.getElementById("sync-reauth-state").hidden = true;
- document.getElementById("sync-setup-state").hidden = true;
- document.getElementById("sync-syncnow-state").hidden = false;
-
- let syncPanel = document.getElementById("PanelUI-remotetabs");
- let links = syncPanel.querySelectorAll(".remotetabs-promo-link");
-
- is(links.length, 2, "found 2 links as expected");
-
- // test each link and left and middle mouse buttons
- for (let link of links) {
- for (let button = 0; button < 2; button++) {
- yield PanelUI.show();
- EventUtils.sendMouseEvent({ type: "click", button }, link, window);
- // the panel should have been closed.
- ok(!isPanelUIOpen(), "click closed the panel");
- // should be a new tab - wait for the load.
- is(gBrowser.tabs.length, 2, "there's a new tab");
- yield new Promise(resolve => {
- if (gBrowser.selectedBrowser.currentURI.spec == "about:blank") {
- gBrowser.selectedBrowser.addEventListener("load", function(e) {
- resolve();
- }, {capture: true, once: true});
- return;
- }
- // the new tab has already transitioned away from about:blank so we
- // are good to go.
- resolve();
- });
-
- let os = link.getAttribute("mobile-promo-os");
- let expectedUrl = `http://example.com/?os=${os}&tail=synced-tabs`;
- is(gBrowser.selectedBrowser.currentURI.spec, expectedUrl, "correct URL");
- gBrowser.removeTab(gBrowser.selectedTab);
- }
- }
-
- // test each link and right mouse button - should be a noop.
- yield PanelUI.show();
- for (let link of links) {
- EventUtils.sendMouseEvent({ type: "click", button: 2 }, link, window);
- // the panel should still be open
- ok(isPanelUIOpen(), "panel remains open after right-click");
- is(gBrowser.tabs.length, 1, "no new tab was opened");
- }
- PanelUI.hide();
-
- Services.prefs.clearUserPref("identity.mobilepromo.android");
- Services.prefs.clearUserPref("identity.mobilepromo.ios");
-});
-
-// Test the "Sync Now" button
-add_task(function* () {
- mockedInternal.getTabClients = () => [];
- mockedInternal.syncTabs = () => {
- return Promise.resolve();
- }
-
- // configure our broadcasters so we are in the right state.
- document.getElementById("sync-reauth-state").hidden = true;
- document.getElementById("sync-setup-state").hidden = true;
- document.getElementById("sync-syncnow-state").hidden = false;
-
- yield PanelUI.show();
- document.getElementById("sync-button").click();
- let syncPanel = document.getElementById("PanelUI-remotetabs");
- ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
-
- let subpanel = document.getElementById("PanelUI-remotetabs-main")
- ok(!subpanel.hidden, "main pane is visible");
- let deck = document.getElementById("PanelUI-remotetabs-deck");
-
- // The widget is still fetching tabs, as we've neutered everything that
- // provides them
- is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
-
- let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
-
- let didSync = false;
- let oldDoSync = gSyncUI.doSync;
- gSyncUI.doSync = function() {
- didSync = true;
- mockedInternal.hasSyncedThisSession = true;
- gSyncUI.doSync = oldDoSync;
- }
- syncNowButton.click();
- ok(didSync, "clicking the button called the correct function");
-
- // Tell the widget there are tabs available, but with zero clients.
- mockedInternal.getTabClients = () => {
- return Promise.resolve([]);
- }
- yield updateTabsPanel();
- // The UI should be showing the "no clients" pane.
- is(deck.selectedIndex, DECKINDEX_NOCLIENTS, "no-clients deck entry is visible");
-
- // Tell the widget there are tabs available - we have 3 clients, one with no
- // tabs.
- mockedInternal.getTabClients = () => {
- return Promise.resolve([
- {
- id: "guid_mobile",
- type: "client",
- name: "My Phone",
- tabs: [],
- },
- {
- id: "guid_desktop",
- type: "client",
- name: "My Desktop",
- tabs: [
- {
- title: "http://example.com/10",
- lastUsed: 10, // the most recent
- },
- {
- title: "http://example.com/1",
- lastUsed: 1, // the least recent.
- },
- {
- title: "http://example.com/5",
- lastUsed: 5,
- },
- ],
- },
- {
- id: "guid_second_desktop",
- name: "My Other Desktop",
- tabs: [
- {
- title: "http://example.com/6",
- lastUsed: 6,
- }
- ],
- },
- ]);
- };
- yield updateTabsPanel();
-
- // The UI should be showing tabs!
- is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible");
- let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
- let node = tabList.firstChild;
- // First entry should be the client with the most-recent tab.
- is(node.getAttribute("itemtype"), "client", "node is a client entry");
- is(node.textContent, "My Desktop", "correct client");
- // Next entry is the most-recent tab
- node = node.nextSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/10");
-
- // Next entry is the next-most-recent tab
- node = node.nextSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/5");
-
- // Next entry is the least-recent tab from the first client.
- node = node.nextSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/1");
-
- // Next is a menuseparator between the clients.
- node = node.nextSibling;
- is(node.nodeName, "menuseparator");
-
- // Next is the client with 1 tab.
- node = node.nextSibling;
- is(node.getAttribute("itemtype"), "client", "node is a client entry");
- is(node.textContent, "My Other Desktop", "correct client");
- // Its single tab
- node = node.nextSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/6");
-
- // Next is a menuseparator between the clients.
- node = node.nextSibling;
- is(node.nodeName, "menuseparator");
-
- // Next is the client with no tab.
- node = node.nextSibling;
- is(node.getAttribute("itemtype"), "client", "node is a client entry");
- is(node.textContent, "My Phone", "correct client");
- // There is a single node saying there's no tabs for the client.
- node = node.nextSibling;
- is(node.nodeName, "label", "node is a label");
- is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client");
-
- node = node.nextSibling;
- is(node, null, "no more entries");
-});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -0,0 +1,427 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+let {SyncedTabs} = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
+
+// These are available on the widget implementation, but it seems impossible
+// to grab that impl at runtime.
+const DECKINDEX_TABS = 0;
+const DECKINDEX_TABSDISABLED = 1;
+const DECKINDEX_FETCHING = 2;
+const DECKINDEX_NOCLIENTS = 3;
+
+var initialLocation = gBrowser.currentURI.spec;
+var newTab = null;
+
+// A helper to notify there are new tabs. Returns a promise that is resolved
+// once the UI has been updated.
+function updateTabsPanel() {
+ let promiseTabsUpdated = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+ Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED, null);
+ return promiseTabsUpdated;
+}
+
+// This is the mock we use for SyncedTabs.jsm - tests may override various
+// functions.
+let mockedInternal = {
+ get isConfiguredToSyncTabs() { return true; },
+ getTabClients() { return Promise.resolve([]); },
+ syncTabs() { return Promise.resolve(); },
+ hasSyncedThisSession: false,
+};
+
+
+add_task(function* setup() {
+ let oldInternal = SyncedTabs._internal;
+ SyncedTabs._internal = mockedInternal;
+
+ // This test hacks some observer states to simulate a user being signed
+ // in to Sync - restore them when the test completes.
+ let initialObserverStates = {};
+ for (let id of ["sync-reauth-state", "sync-setup-state", "sync-syncnow-state"]) {
+ initialObserverStates[id] = document.getElementById(id).hidden;
+ }
+
+ registerCleanupFunction(() => {
+ SyncedTabs._internal = oldInternal;
+ for (let [id, initial] of Object.entries(initialObserverStates)) {
+ document.getElementById(id).hidden = initial;
+ }
+ });
+});
+
+// The test expects the about:preferences#sync page to open in the current tab
+function* openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
+ info("Check Sync button functionality");
+ Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", "http://example.com/");
+
+ // check the button's functionality
+ yield PanelUI.show();
+
+ if (entryPoint == "uitour") {
+ UITour.tourBrowsersByWindow.set(window, new Set());
+ UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
+ }
+
+ let syncButton = document.getElementById("sync-button");
+ ok(syncButton, "The Sync button was added to the Panel Menu");
+
+ let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+ syncButton.click();
+ yield tabsUpdatedPromise;
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
+
+ // Sync is not configured - verify that state is reflected.
+ let subpanel = document.getElementById(expectedPanelId)
+ ok(!subpanel.hidden, "sync setup element is visible");
+
+ // Find and click the "setup" button.
+ let setupButton = subpanel.querySelector(".PanelUI-remotetabs-prefs-button");
+ setupButton.click();
+
+ let deferred = Promise.defer();
+ let handler = (e) => {
+ if (e.originalTarget != gBrowser.selectedBrowser.contentDocument ||
+ e.target.location.href == "about:blank") {
+ info("Skipping spurious 'load' event for " + e.target.location.href);
+ return;
+ }
+ gBrowser.selectedBrowser.removeEventListener("load", handler, true);
+ deferred.resolve();
+ }
+ gBrowser.selectedBrowser.addEventListener("load", handler, true);
+
+ yield deferred.promise;
+ newTab = gBrowser.selectedTab;
+
+ is(gBrowser.currentURI.spec, "about:preferences?entrypoint=" + entryPoint + "#sync",
+ "Firefox Sync preference page opened with `menupanel` entrypoint");
+ ok(!isPanelUIOpen(), "The panel closed");
+
+ if (isPanelUIOpen()) {
+ yield panelUIHide();
+ }
+}
+
+function panelUIHide() {
+ let panelHidePromise = promisePanelHidden(window);
+ PanelUI.hide();
+ return panelHidePromise;
+}
+
+function* asyncCleanup() {
+ Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
+ // reset the panel UI to the default state
+ yield resetCustomization();
+ ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
+
+ // restore the tabs
+ gBrowser.addTab(initialLocation);
+ gBrowser.removeTab(newTab);
+ UITour.tourBrowsersByWindow.delete(window);
+}
+
+// When Sync is not setup.
+add_task(function* () {
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = false;
+ document.getElementById("sync-syncnow-state").hidden = true;
+ yield openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs")
+});
+add_task(asyncCleanup);
+
+// When Sync is configured in a "needs reauthentication" state.
+add_task(function* () {
+ // configure our broadcasters so we are in the right state.
+ document.getElementById("sync-reauth-state").hidden = false;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = true;
+ yield openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs")
+});
+
+// Test the mobile promo links
+add_task(function* () {
+ // change the preferences for the mobile links.
+ Services.prefs.setCharPref("identity.mobilepromo.android", "http://example.com/?os=android&tail=");
+ Services.prefs.setCharPref("identity.mobilepromo.ios", "http://example.com/?os=ios&tail=");
+
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = false;
+
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ let links = syncPanel.querySelectorAll(".remotetabs-promo-link");
+
+ is(links.length, 2, "found 2 links as expected");
+
+ // test each link and left and middle mouse buttons
+ for (let link of links) {
+ for (let button = 0; button < 2; button++) {
+ yield PanelUI.show();
+ EventUtils.sendMouseEvent({ type: "click", button }, link, window);
+ // the panel should have been closed.
+ ok(!isPanelUIOpen(), "click closed the panel");
+ // should be a new tab - wait for the load.
+ is(gBrowser.tabs.length, 2, "there's a new tab");
+ yield new Promise(resolve => {
+ if (gBrowser.selectedBrowser.currentURI.spec == "about:blank") {
+ gBrowser.selectedBrowser.addEventListener("load", function(e) {
+ resolve();
+ }, {capture: true, once: true});
+ return;
+ }
+ // the new tab has already transitioned away from about:blank so we
+ // are good to go.
+ resolve();
+ });
+
+ let os = link.getAttribute("mobile-promo-os");
+ let expectedUrl = `http://example.com/?os=${os}&tail=synced-tabs`;
+ is(gBrowser.selectedBrowser.currentURI.spec, expectedUrl, "correct URL");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ }
+
+ // test each link and right mouse button - should be a noop.
+ yield PanelUI.show();
+ for (let link of links) {
+ EventUtils.sendMouseEvent({ type: "click", button: 2 }, link, window);
+ // the panel should still be open
+ ok(isPanelUIOpen(), "panel remains open after right-click");
+ is(gBrowser.tabs.length, 1, "no new tab was opened");
+ }
+ yield panelUIHide();
+
+ Services.prefs.clearUserPref("identity.mobilepromo.android");
+ Services.prefs.clearUserPref("identity.mobilepromo.ios");
+});
+
+// Test the "Sync Now" button
+add_task(function* () {
+ // configure our broadcasters so we are in the right state.
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = false;
+
+ yield PanelUI.show();
+ let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+ document.getElementById("sync-button").click();
+ yield tabsUpdatedPromise;
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
+
+ let subpanel = document.getElementById("PanelUI-remotetabs-main")
+ ok(!subpanel.hidden, "main pane is visible");
+ let deck = document.getElementById("PanelUI-remotetabs-deck");
+
+ // The widget is still fetching tabs, as we've neutered everything that
+ // provides them
+ is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
+
+ let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
+
+ let didSync = false;
+ let oldDoSync = gSyncUI.doSync;
+ gSyncUI.doSync = function() {
+ didSync = true;
+ mockedInternal.hasSyncedThisSession = true;
+ gSyncUI.doSync = oldDoSync;
+ }
+ syncNowButton.click();
+ ok(didSync, "clicking the button called the correct function");
+
+ // Tell the widget there are tabs available, but with zero clients.
+ mockedInternal.getTabClients = () => {
+ return Promise.resolve([]);
+ }
+ yield updateTabsPanel();
+ // The UI should be showing the "no clients" pane.
+ is(deck.selectedIndex, DECKINDEX_NOCLIENTS, "no-clients deck entry is visible");
+
+ // Tell the widget there are tabs available - we have 3 clients, one with no
+ // tabs.
+ mockedInternal.getTabClients = () => {
+ return Promise.resolve([
+ {
+ id: "guid_mobile",
+ type: "client",
+ name: "My Phone",
+ tabs: [],
+ },
+ {
+ id: "guid_desktop",
+ type: "client",
+ name: "My Desktop",
+ tabs: [
+ {
+ title: "http://example.com/10",
+ lastUsed: 10, // the most recent
+ },
+ {
+ title: "http://example.com/1",
+ lastUsed: 1, // the least recent.
+ },
+ {
+ title: "http://example.com/5",
+ lastUsed: 5,
+ },
+ ],
+ },
+ {
+ id: "guid_second_desktop",
+ name: "My Other Desktop",
+ tabs: [
+ {
+ title: "http://example.com/6",
+ lastUsed: 6,
+ }
+ ],
+ },
+ ]);
+ };
+ yield updateTabsPanel();
+
+ // The UI should be showing tabs!
+ is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible");
+ let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
+ let node = tabList.firstChild;
+ // First entry should be the client with the most-recent tab.
+ is(node.getAttribute("itemtype"), "client", "node is a client entry");
+ is(node.textContent, "My Desktop", "correct client");
+ // Next entry is the most-recent tab
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "http://example.com/10");
+
+ // Next entry is the next-most-recent tab
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "http://example.com/5");
+
+ // Next entry is the least-recent tab from the first client.
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "http://example.com/1");
+
+ // Next is a menuseparator between the clients.
+ node = node.nextSibling;
+ is(node.nodeName, "menuseparator");
+
+ // Next is the client with 1 tab.
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "client", "node is a client entry");
+ is(node.textContent, "My Other Desktop", "correct client");
+ // Its single tab
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "http://example.com/6");
+
+ // Next is a menuseparator between the clients.
+ node = node.nextSibling;
+ is(node.nodeName, "menuseparator");
+
+ // Next is the client with no tab.
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "client", "node is a client entry");
+ is(node.textContent, "My Phone", "correct client");
+ // There is a single node saying there's no tabs for the client.
+ node = node.nextSibling;
+ is(node.nodeName, "label", "node is a label");
+ is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client");
+
+ node = node.nextSibling;
+ is(node, null, "no more entries");
+
+ yield panelUIHide();
+});
+
+// Test the pagination capabilities (Show More/All tabs)
+add_task(function* () {
+ mockedInternal.getTabClients = () => {
+ return Promise.resolve([
+ {
+ id: "guid_desktop",
+ type: "client",
+ name: "My Desktop",
+ tabs: function() {
+ let allTabsDesktop = [];
+ // We choose 77 tabs, because TABS_PER_PAGE is 25, which means
+ // on the second to last page we should have 22 items shown
+ // (because we have to show at least NEXT_PAGE_MIN_TABS=5 tabs on the last page)
+ for (let i = 1; i <= 77; i++) {
+ allTabsDesktop.push({ title: "Tab #" + i });
+ }
+ return allTabsDesktop;
+ }(),
+ }
+ ]);
+ };
+
+ // configure our broadcasters so we are in the right state.
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = false;
+
+ yield PanelUI.show();
+ let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+ document.getElementById("sync-button").click();
+ yield tabsUpdatedPromise;
+
+ // Check pre-conditions
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
+ let subpanel = document.getElementById("PanelUI-remotetabs-main")
+ ok(!subpanel.hidden, "main pane is visible");
+ let deck = document.getElementById("PanelUI-remotetabs-deck");
+ is(deck.selectedIndex, DECKINDEX_TABS, "we should be showing tabs");
+
+ function checkTabsPage(tabsShownCount, showMoreLabel) {
+ let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
+ let node = tabList.firstChild;
+ is(node.getAttribute("itemtype"), "client", "node is a client entry");
+ is(node.textContent, "My Desktop", "correct client");
+ for (let i = 0; i < tabsShownCount; i++) {
+ node = node.nextSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "Tab #" + (i + 1), "the tab is the correct one");
+ }
+ let showMoreButton;
+ if (showMoreLabel) {
+ node = showMoreButton = node.nextSibling;
+ is(node.getAttribute("itemtype"), "showmorebutton", "node is a show more button");
+ is(node.getAttribute("label"), showMoreLabel);
+ }
+ node = node.nextSibling;
+ is(node, null, "no more entries");
+
+ return showMoreButton;
+ }
+
+ let showMoreButton;
+ function clickShowMoreButton() {
+ let promise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+ showMoreButton.click();
+ return promise;
+ }
+
+ showMoreButton = checkTabsPage(25, "Show More");
+ yield clickShowMoreButton();
+
+ showMoreButton = checkTabsPage(50, "Show More");
+ yield clickShowMoreButton();
+
+ showMoreButton = checkTabsPage(72, "Show All");
+ yield clickShowMoreButton();
+
+ checkTabsPage(77, null);
+
+ yield panelUIHide();
+});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -353,16 +353,24 @@ These should match what Safari and other
<!ENTITY appMenuHistory.restoreSession.label "Restore Previous Session">
<!ENTITY appMenuHistory.viewSidebar.label "View History Sidebar">
<!ENTITY appMenuHelp.tooltip "Open Help Menu">
<!ENTITY appMenuRemoteTabs.label "Synced Tabs">
<!-- LOCALIZATION NOTE (appMenuRemoteTabs.notabs.label): This is shown beneath
the name of a device when that device has no open tabs -->
<!ENTITY appMenuRemoteTabs.notabs.label "No open tabs">
+<!-- LOCALIZATION NOTE (appMenuRemoteTabs.showMore.label, appMenuRemoteTabs.showMore.tooltip):
+ This is shown after the tabs list if we can display more tabs by clicking on the button -->
+<!ENTITY appMenuRemoteTabs.showMore.label "Show More">
+<!ENTITY appMenuRemoteTabs.showMore.tooltip "Show more tabs from this device">
+<!-- LOCALIZATION NOTE (appMenuRemoteTabs.showAll.label, appMenuRemoteTabs.showAll.tooltip):
+ This is shown after the tabs list if we can all the remaining tabs by clicking on the button -->
+<!ENTITY appMenuRemoteTabs.showAll.label "Show All">
+<!ENTITY appMenuRemoteTabs.showAll.tooltip "Show all tabs from this device">
<!-- LOCALIZATION NOTE (appMenuRemoteTabs.tabsnotsyncing.label): This is shown
when Sync is configured but syncing tabs is disabled. -->
<!ENTITY appMenuRemoteTabs.tabsnotsyncing.label "Turn on tab syncing to view a list of tabs from your other devices.">
<!-- 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?">
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -1238,29 +1238,29 @@ menuitem.panel-subview-footer@menuStateA
.subviewbutton > .menu-accel-container {
-moz-box-pack: start;
margin-inline-start: 10px;
margin-inline-end: auto;
color: GrayText;
}
-#PanelUI-remotetabs-tabslist > toolbarbutton,
+#PanelUI-remotetabs-tabslist > toolbarbutton[itemtype="tab"],
#PanelUI-historyItems > toolbarbutton {
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
}
@media (min-resolution: 1.1dppx) {
- #PanelUI-remotetabs-tabslist > toolbarbutton,
+ #PanelUI-remotetabs-tabslist > toolbarbutton[itemtype="tab"],
#PanelUI-historyItems > toolbarbutton {
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon@2x.png");
}
}
-#PanelUI-remotetabs-tabslist > toolbarbutton > .toolbarbutton-icon,
+#PanelUI-remotetabs-tabslist > toolbarbutton[itemtype="tab"] > .toolbarbutton-icon,
#PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
#PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
#PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
width: 16px;
height: 16px;
}
toolbarbutton[panel-multiview-anchor="true"],
--- a/services/sync/modules/SyncedTabs.jsm
+++ b/services/sync/modules/SyncedTabs.jsm
@@ -267,26 +267,23 @@ this.SyncedTabs = {
// resolves when the sync is complete, but there's no resolved value -
// callers should be listening for TOPIC_TABS_CHANGED.
// If |force| is true we always sync. If false, we only sync if the most
// recent sync wasn't "recently".
syncTabs(force) {
return this._internal.syncTabs(force);
},
- sortTabClientsByLastUsed(clients, maxTabs = Infinity) {
- // First sort and filter the list of tabs for each client. Note that
+ sortTabClientsByLastUsed(clients) {
+ // First sort the list of tabs for each client. Note that
// this module promises that the objects it returns are never
// shared, so we are free to mutate those objects directly.
for (let client of clients) {
let tabs = client.tabs;
tabs.sort((a, b) => b.lastUsed - a.lastUsed);
- if (Number.isFinite(maxTabs)) {
- client.tabs = tabs.slice(0, maxTabs);
- }
}
// Now sort the clients - the clients are sorted in the order of the
// most recent tab for that client (ie, it is important the tabs for
// each client are already sorted.)
clients.sort((a, b) => {
if (a.tabs.length == 0) {
return 1; // b comes first.
}