diff options
Diffstat (limited to 'browser/components/customizableui/test/browser_synced_tabs_menu.js')
-rw-r--r-- | browser/components/customizableui/test/browser_synced_tabs_menu.js | 523 |
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(); +}); |