summaryrefslogtreecommitdiffstats
path: root/browser/components/customizableui/test/browser_synced_tabs_menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/customizableui/test/browser_synced_tabs_menu.js')
-rw-r--r--browser/components/customizableui/test/browser_synced_tabs_menu.js523
1 files changed, 523 insertions, 0 deletions
diff --git a/browser/components/customizableui/test/browser_synced_tabs_menu.js b/browser/components/customizableui/test/browser_synced_tabs_menu.js
new file mode 100644
index 0000000000..ff60167fea
--- /dev/null
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -0,0 +1,523 @@
+/* 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);
+
+const { FxAccounts } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+let { SyncedTabs } = ChromeUtils.importESModule(
+ "resource://services-sync/SyncedTabs.sys.mjs"
+);
+let { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ UITour: "resource:///modules/UITour.sys.mjs",
+});
+
+const DECKINDEX_TABS = 0;
+const DECKINDEX_FETCHING = 1;
+const DECKINDEX_TABSDISABLED = 2;
+const DECKINDEX_NOCLIENTS = 3;
+
+const SAMPLE_TAB_URL = "https://example.com/";
+
+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);
+ 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_setup(async function () {
+ const getSignedInUser = FxAccounts.config.getSignedInUser;
+ FxAccounts.config.getSignedInUser = async () =>
+ Promise.resolve({ uid: "uid", email: "foo@bar.com" });
+ Services.prefs.setCharPref(
+ "identity.fxaccounts.remote.root",
+ "https://example.com/"
+ );
+
+ let oldInternal = SyncedTabs._internal;
+ SyncedTabs._internal = mockedInternal;
+
+ let origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
+ // Sync start-up will interfere with our tests, don't let UIState send UI updates.
+ UIState._internal.notifyStateUpdated = () => {};
+
+ // Force gSync initialization
+ gSync.init();
+
+ registerCleanupFunction(() => {
+ FxAccounts.config.getSignedInUser = getSignedInUser;
+ Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
+ UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
+ SyncedTabs._internal = oldInternal;
+ });
+});
+
+// The test expects the about:preferences#sync page to open in the current tab
+async function openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
+ info("Check Sync button functionality");
+ CustomizableUI.addWidgetToArea(
+ "sync-button",
+ CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
+ );
+
+ await waitForOverflowButtonShown();
+
+ // check the button's functionality
+ await document.getElementById("nav-bar").overflowable.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();
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
+ await Promise.all([tabsUpdatedPromise, viewShownPromise]);
+ ok(syncPanel.getAttribute("visible"), "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-button");
+ setupButton.click();
+
+ await new Promise(resolve => {
+ let handler = async 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);
+ resolve();
+ };
+ gBrowser.selectedBrowser.addEventListener("load", handler, true);
+ });
+ newTab = gBrowser.selectedTab;
+
+ is(
+ gBrowser.currentURI.spec,
+ "about:preferences?entrypoint=" + entryPoint + "#sync",
+ "Firefox Sync preference page opened with `menupanel` entrypoint"
+ );
+ ok(!isOverflowOpen(), "The panel closed");
+
+ if (isOverflowOpen()) {
+ await hideOverflow();
+ }
+}
+
+function hideOverflow() {
+ let panelHidePromise = promiseOverflowHidden(window);
+ PanelUI.overflowPanel.hidePopup();
+ return panelHidePromise;
+}
+
+async function asyncCleanup() {
+ // reset the panel UI to the default state
+ await resetCustomization();
+ ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
+
+ // restore the tabs
+ BrowserTestUtils.addTab(gBrowser, initialLocation);
+ gBrowser.removeTab(newTab);
+ UITour.tourBrowsersByWindow.delete(window);
+}
+
+// When Sync is not setup.
+add_task(async function () {
+ gSync.updateAllUI({ status: UIState.STATUS_NOT_CONFIGURED });
+ await openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs");
+});
+add_task(asyncCleanup);
+
+// When an account is connected by Sync is not enabled.
+add_task(async function () {
+ gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, syncEnabled: false });
+ await openPrefsFromMenuPanel(
+ "PanelUI-remotetabs-syncdisabled",
+ "synced-tabs"
+ );
+});
+add_task(asyncCleanup);
+
+// When Sync is configured in an unverified state.
+add_task(async function () {
+ gSync.updateAllUI({
+ status: UIState.STATUS_NOT_VERIFIED,
+ email: "foo@bar.com",
+ });
+ await openPrefsFromMenuPanel("PanelUI-remotetabs-unverified", "synced-tabs");
+});
+add_task(asyncCleanup);
+
+// When Sync is configured in a "needs reauthentication" state.
+add_task(async function () {
+ gSync.updateAllUI({
+ status: UIState.STATUS_LOGIN_FAILED,
+ email: "foo@bar.com",
+ });
+ await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs");
+});
+
+// Test the Connect Another Device button
+add_task(async function () {
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ });
+
+ let button = document.getElementById(
+ "PanelUI-remotetabs-connect-device-button"
+ );
+ ok(button, "found the button");
+
+ await document.getElementById("nav-bar").overflowable.show();
+ let expectedUrl =
+ "https://example.com/connect_another_device?context=" +
+ "fx_desktop_v3&entrypoint=synced-tabs&service=sync&uid=uid&email=foo%40bar.com";
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedUrl);
+ button.click();
+ // the panel should have been closed.
+ ok(!isOverflowOpen(), "click closed the panel");
+ await promiseTabOpened;
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
+
+// Test the "Sync Now" button
+add_task(async function () {
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ });
+
+ await document.getElementById("nav-bar").overflowable.show();
+ let tabsUpdatedPromise = promiseObserverNotified(
+ "synced-tabs-menu:test:tabs-updated"
+ );
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
+ let syncButton = document.getElementById("sync-button");
+ syncButton.click();
+ await Promise.all([tabsUpdatedPromise, viewShownPromise]);
+ ok(syncPanel.getAttribute("visible"), "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");
+
+ // Tell the widget there are tabs available, but with zero clients.
+ mockedInternal.getTabClients = () => {
+ return Promise.resolve([]);
+ };
+ mockedInternal.hasSyncedThisSession = true;
+ await 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",
+ lastModified: 1492201200,
+ tabs: [],
+ },
+ {
+ id: "guid_desktop",
+ type: "client",
+ name: "My Desktop",
+ lastModified: 1492201200,
+ 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",
+ lastModified: 1492201200,
+ tabs: [
+ {
+ title: "http://example.com/6",
+ lastUsed: 6,
+ },
+ ],
+ },
+ ]);
+ };
+ await 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.firstElementChild;
+ // First entry should be the client with the most-recent tab.
+ is(node.nodeName, "vbox");
+ let currentClient = node;
+ node = node.firstElementChild;
+ 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.nextElementSibling;
+ 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.nextElementSibling;
+ 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.nextElementSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "http://example.com/1");
+ node = node.nextElementSibling;
+ is(node, null, "no more siblings");
+
+ // Next is a toolbarseparator between the clients.
+ node = currentClient.nextElementSibling;
+ is(node.nodeName, "toolbarseparator");
+
+ // Next is the container for client 2.
+ node = node.nextElementSibling;
+ is(node.nodeName, "vbox");
+ currentClient = node;
+
+ // Next is the client with 1 tab.
+ node = node.firstElementChild;
+ is(node.getAttribute("itemtype"), "client", "node is a client entry");
+ is(node.textContent, "My Other Desktop", "correct client");
+ // Its single tab
+ node = node.nextElementSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.getAttribute("label"), "http://example.com/6");
+ node = node.nextElementSibling;
+ is(node, null, "no more siblings");
+
+ // Next is a toolbarseparator between the clients.
+ node = currentClient.nextElementSibling;
+ is(node.nodeName, "toolbarseparator");
+
+ // Next is the container for client 3.
+ node = node.nextElementSibling;
+ is(node.nodeName, "vbox");
+ currentClient = node;
+
+ // Next is the client with no tab.
+ node = node.firstElementChild;
+ 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.nextElementSibling;
+ is(node.nodeName, "label", "node is a label");
+ is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client");
+
+ node = node.nextElementSibling;
+ is(node, null, "no more siblings");
+ is(currentClient.nextElementSibling, null, "no more clients");
+
+ // Check accessibility. There should be containers for each client, with an
+ // aria attribute that identifies the client name.
+ let clientContainers = [
+ ...tabList.querySelectorAll("[aria-labelledby]").values(),
+ ];
+ let labelIds = clientContainers.map(container =>
+ container.getAttribute("aria-labelledby")
+ );
+ let labels = labelIds.map(id => document.getElementById(id).textContent);
+ Assert.deepEqual(labels.sort(), [
+ "My Desktop",
+ "My Other Desktop",
+ "My Phone",
+ ]);
+
+ let didSync = false;
+ let oldDoSync = gSync.doSync;
+ gSync.doSync = function () {
+ didSync = true;
+ gSync.doSync = oldDoSync;
+ };
+
+ let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
+ is(syncNowButton.disabled, false);
+ syncNowButton.click();
+ ok(didSync, "clicking the button called the correct function");
+
+ await hideOverflow();
+});
+
+// Test the pagination capabilities (Show More/All tabs)
+add_task(async function () {
+ mockedInternal.getTabClients = () => {
+ return Promise.resolve([
+ {
+ id: "guid_desktop",
+ type: "client",
+ name: "My Desktop",
+ lastModified: 1492201200,
+ 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, url: SAMPLE_TAB_URL });
+ }
+ return allTabsDesktop;
+ })(),
+ },
+ ]);
+ };
+
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ lastSync: new Date(),
+ email: "foo@bar.com",
+ });
+
+ await document.getElementById("nav-bar").overflowable.show();
+ let tabsUpdatedPromise = promiseObserverNotified(
+ "synced-tabs-menu:test:tabs-updated"
+ );
+ let syncPanel = document.getElementById("PanelUI-remotetabs");
+ let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown");
+ let syncButton = document.getElementById("sync-button");
+ syncButton.click();
+ await Promise.all([tabsUpdatedPromise, viewShownPromise]);
+
+ // Check pre-conditions
+ ok(syncPanel.getAttribute("visible"), "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.firstElementChild.firstElementChild;
+ 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.nextElementSibling;
+ is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(
+ node.getAttribute("label"),
+ "Tab #" + (i + 1),
+ "the tab is the correct one"
+ );
+ is(
+ node.getAttribute("targetURI"),
+ SAMPLE_TAB_URL,
+ "url is the correct one"
+ );
+ }
+ let showMoreButton;
+ if (showMoreLabel) {
+ node = showMoreButton = node.nextElementSibling;
+ is(
+ node.getAttribute("itemtype"),
+ "showmorebutton",
+ "node is a show more button"
+ );
+ is(node.getAttribute("label"), showMoreLabel);
+ }
+ node = node.nextElementSibling;
+ is(node, null, "no more entries");
+
+ return showMoreButton;
+ }
+
+ async function checkCanOpenURL() {
+ let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
+ let node = tabList.firstElementChild.firstElementChild.nextElementSibling;
+ let promiseTabOpened = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ SAMPLE_TAB_URL
+ );
+ node.click();
+ await promiseTabOpened;
+ }
+
+ let showMoreButton;
+ function clickShowMoreButton() {
+ let promise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+ showMoreButton.click();
+ return promise;
+ }
+
+ showMoreButton = checkTabsPage(25, "Show More Tabs");
+ await clickShowMoreButton();
+
+ checkTabsPage(77, null);
+ /* calling this will close the overflow menu */
+ await checkCanOpenURL();
+});