diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/base/content/test/sync | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/sync')
-rw-r--r-- | browser/base/content/test/sync/browser.toml | 16 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_contextmenu_sendpage.js | 474 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_contextmenu_sendtab.js | 362 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_fxa_badge.js | 70 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_fxa_web_channel.html | 158 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_fxa_web_channel.js | 280 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_sync.js | 935 | ||||
-rw-r--r-- | browser/base/content/test/sync/browser_synced_tabs_view.js | 76 | ||||
-rw-r--r-- | browser/base/content/test/sync/head.js | 34 |
9 files changed, 2405 insertions, 0 deletions
diff --git a/browser/base/content/test/sync/browser.toml b/browser/base/content/test/sync/browser.toml new file mode 100644 index 0000000000..caed29af35 --- /dev/null +++ b/browser/base/content/test/sync/browser.toml @@ -0,0 +1,16 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_contextmenu_sendpage.js"] + +["browser_contextmenu_sendtab.js"] + +["browser_fxa_badge.js"] + +["browser_fxa_web_channel.js"] +https_first_disabled = true +support-files = ["browser_fxa_web_channel.html"] + +["browser_sync.js"] + +["browser_synced_tabs_view.js"] diff --git a/browser/base/content/test/sync/browser_contextmenu_sendpage.js b/browser/base/content/test/sync/browser_contextmenu_sendpage.js new file mode 100644 index 0000000000..a80cf8a1d0 --- /dev/null +++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js @@ -0,0 +1,474 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const fxaDevices = [ + { + id: 1, + name: "Foo", + availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "baz" }, + lastAccessTime: Date.now(), + }, + { + id: 2, + name: "Bar", + availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "boo" }, + lastAccessTime: Date.now() + 60000, // add 30min + }, + { + id: 3, + name: "Baz", + clientRecord: "bar", + lastAccessTime: Date.now() + 120000, // add 60min + }, // Legacy send tab target (no availableCommands). + { id: 4, name: "Homer" }, // Incompatible target. +]; + +add_setup(async function () { + await promiseSyncReady(); + await Services.search.init(); + // gSync.init() is called in a requestIdleCallback. Force its initialization. + gSync.init(); + sinon + .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId") + .callsFake(fxaDeviceId => { + let target = fxaDevices.find(c => c.id == fxaDeviceId); + return target ? target.clientRecord : null; + }); + sinon.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop"); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); +}); + +add_task(async function test_page_contextmenu() { + const sandbox = setupSendTabMocks({ fxaDevices }); + + await openContentContextMenu("#moztext", "context-sendpagetodevice"); + is( + document.getElementById("context-sendpagetodevice").hidden, + false, + "Send page to device is shown" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send page to device is enabled" + ); + checkPopup([ + { label: "Bar" }, + { label: "Foo" }, + "----", + { label: "Send to All Devices" }, + { label: "Manage Devices..." }, + ]); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_link_contextmenu() { + const sandbox = setupSendTabMocks({ fxaDevices }); + let expectation = sandbox + .mock(gSync) + .expects("sendTabToDevice") + .once() + .withExactArgs( + "https://www.example.org/", + [fxaDevices[1]], + "Click on me!!" + ); + + // Add a link to the page + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let a = content.document.createElement("a"); + a.href = "https://www.example.org"; + a.id = "testingLink"; + a.textContent = "Click on me!!"; + content.document.body.appendChild(a); + }); + + let contextMenu = await openContentContextMenu( + "#testingLink", + "context-sendlinktodevice", + "context-sendlinktodevice-popup" + ); + + let expectedArray = ["context-openlinkintab"]; + + if ( + Services.prefs.getBoolPref("privacy.userContext.enabled") && + ContextualIdentityService.getPublicIdentities().length + ) { + expectedArray.push("context-openlinkinusercontext-menu"); + } + + expectedArray.push( + "context-openlink", + "context-openlinkprivate", + "context-sep-open", + "context-bookmarklink", + "context-savelink", + "context-savelinktopocket", + "context-copylink" + ); + + if ( + Services.prefs.getBoolPref("privacy.query_stripping.strip_on_share.enabled") + ) { + expectedArray.push("context-stripOnShareLink"); + } + + expectedArray.push( + "context-sendlinktodevice", + "context-sep-sendlinktodevice", + "context-searchselect", + "frame-sep" + ); + + if ( + Services.prefs.getBoolPref("devtools.accessibility.enabled", true) && + (Services.prefs.getBoolPref("devtools.everOpened", false) || + Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0) + ) { + expectedArray.push("context-inspect-a11y"); + } + + expectedArray.push("context-inspect"); + + let menu = document.getElementById("contentAreaContextMenu"); + + for (let i = 0, j = 0; i < menu.children.length; i++) { + let item = menu.children[i]; + if (item.hidden) { + continue; + } + Assert.equal( + item.id, + expectedArray[j], + "Ids in context menu match expected values" + ); + j++; + } + + is( + document.getElementById("context-sendlinktodevice").hidden, + false, + "Send link to device is shown" + ); + is( + document.getElementById("context-sendlinktodevice").disabled, + false, + "Send link to device is enabled" + ); + contextMenu.activateItem( + document + .getElementById("context-sendlinktodevice-popup") + .querySelector("menuitem") + ); + await hideContentContextMenu(); + + expectation.verify(); + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_no_remote_clients() { + const sandbox = setupSendTabMocks({ fxaDevices: [] }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send tab to device is enabled" + ); + checkPopup(); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_one_remote_client() { + const sandbox = setupSendTabMocks({ + fxaDevices: [ + { + id: 1, + name: "Foo", + availableCommands: { + "https://identity.mozilla.com/cmd/open-uri": "baz", + }, + }, + ], + }); + + await openContentContextMenu("#moztext", "context-sendpagetodevice"); + is( + document.getElementById("context-sendpagetodevice").hidden, + false, + "Send page to device is shown" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send page to device is enabled" + ); + checkPopup([{ label: "Foo" }]); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_not_sendable() { + const sandbox = setupSendTabMocks({ fxaDevices, isSendableURI: false }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + true, + "Send page to device is disabled" + ); + checkPopup(); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_not_synced_yet() { + const sandbox = setupSendTabMocks({ fxaDevices: null }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + true, + "Send page to device is disabled" + ); + checkPopup(); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_sync_not_ready_configured() { + const sandbox = setupSendTabMocks({ syncReady: false }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + true, + "Send page to device is disabled" + ); + checkPopup(); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_sync_not_ready_other_state() { + const sandbox = setupSendTabMocks({ + syncReady: false, + state: UIState.STATUS_NOT_VERIFIED, + }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send page to device is enabled" + ); + checkPopup(); + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_unconfigured() { + const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_CONFIGURED }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send page to device is enabled" + ); + checkPopup(); + + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_not_verified() { + const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_VERIFIED }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send page to device is enabled" + ); + checkPopup(); + + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_login_failed() { + const sandbox = setupSendTabMocks({ state: UIState.STATUS_LOGIN_FAILED }); + + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + is( + document.getElementById("context-sendpagetodevice").disabled, + false, + "Send page to device is enabled" + ); + checkPopup(); + + await hideContentContextMenu(); + + sandbox.restore(); +}); + +add_task(async function test_page_contextmenu_fxa_disabled() { + const getter = sinon.stub(gSync, "FXA_ENABLED").get(() => false); + gSync.onFxaDisabled(); // Would have been called on gSync initialization if FXA_ENABLED had been set. + await openContentContextMenu("#moztext"); + is( + document.getElementById("context-sendpagetodevice").hidden, + true, + "Send page to device is hidden" + ); + await hideContentContextMenu(); + getter.restore(); + [...document.querySelectorAll(".sync-ui-item")].forEach( + e => (e.hidden = false) + ); +}); + +// We are not going to bother testing the visibility of context-sendlinktodevice +// since it uses the exact same code. +// However, browser_contextmenu.js contains tests that verify its presence. + +add_task(async function teardown() { + Weave.Service.clientsEngine.getClientByFxaDeviceId.restore(); + Weave.Service.clientsEngine.getClientType.restore(); + gBrowser.removeCurrentTab(); +}); + +function checkPopup(expectedItems = null) { + const popup = document.getElementById("context-sendpagetodevice-popup"); + if (!expectedItems) { + is(popup.state, "closed", "Popup should be hidden."); + return; + } + const menuItems = popup.children; + for (let i = 0; i < menuItems.length; i++) { + const menuItem = menuItems[i]; + const expectedItem = expectedItems[i]; + if (expectedItem === "----") { + is(menuItem.nodeName, "menuseparator", "Found a separator"); + continue; + } + is(menuItem.nodeName, "menuitem", "Found a menu item"); + // Bug workaround, menuItem.label "…" encoding is different than ours. + is( + menuItem.label.normalize("NFKC"), + expectedItem.label, + "Correct menu item label" + ); + is( + menuItem.disabled, + !!expectedItem.disabled, + "Correct menu item disabled state" + ); + } + // check the length last - the above loop might have given us other clues... + is( + menuItems.length, + expectedItems.length, + "Popup has the expected children count." + ); +} + +async function openContentContextMenu(selector, openSubmenuId = null) { + const contextMenu = document.getElementById("contentAreaContextMenu"); + is(contextMenu.state, "closed", "checking if popup is closed"); + + const 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) { + const menu = document.getElementById(openSubmenuId); + const menuPopup = menu.menupopup; + const menuPopupPromise = BrowserTestUtils.waitForEvent( + menuPopup, + "popupshown" + ); + menu.openMenu(true); + await menuPopupPromise; + } + return contextMenu; +} + +async function hideContentContextMenu() { + const contextMenu = document.getElementById("contentAreaContextMenu"); + const awaitPopupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await awaitPopupHidden; +} diff --git a/browser/base/content/test/sync/browser_contextmenu_sendtab.js b/browser/base/content/test/sync/browser_contextmenu_sendtab.js new file mode 100644 index 0000000000..4922869c1d --- /dev/null +++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js @@ -0,0 +1,362 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kForceOverflowWidthPx = 450; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/general/head.js", + this +); + +const fxaDevices = [ + { + id: 1, + name: "Foo", + availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "baz" }, + lastAccessTime: Date.now(), + }, + { + id: 2, + name: "Bar", + availableCommands: { "https://identity.mozilla.com/cmd/open-uri": "boo" }, + lastAccessTime: Date.now() + 60000, // add 30min + }, + { + id: 3, + name: "Baz", + clientRecord: "bar", + lastAccessTime: Date.now() + 120000, // add 60min + }, // Legacy send tab target (no availableCommands). + { id: 4, name: "Homer" }, // Incompatible target. +]; + +let [testTab] = gBrowser.visibleTabs; + +function updateTabContextMenu(tab = gBrowser.selectedTab) { + let menu = document.getElementById("tabContextMenu"); + var evt = new Event(""); + tab.dispatchEvent(evt); + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test + gBrowser.selectedTab.focus(); + menu.openPopup(tab, "end_after", 0, 0, true, false, evt); + is( + window.TabContextMenu.contextTab, + tab, + "TabContextMenu context is the expected tab" + ); + menu.hidePopup(); +} + +add_setup(async function () { + await promiseSyncReady(); + await Services.search.init(); + // gSync.init() is called in a requestIdleCallback. Force its initialization. + gSync.init(); + sinon + .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId") + .callsFake(fxaDeviceId => { + let target = fxaDevices.find(c => c.id == fxaDeviceId); + return target ? target.clientRecord : null; + }); + sinon.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop"); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + registerCleanupFunction(() => { + gBrowser.removeCurrentTab(); + }); + is(gBrowser.visibleTabs.length, 2, "there are two visible tabs"); +}); + +add_task(async function test_sendTabToDevice_callsFlushLogFile() { + const sandbox = setupSendTabMocks({ fxaDevices }); + updateTabContextMenu(testTab); + await openTabContextMenu("context_sendTabToDevice"); + let promiseObserved = promiseObserver("service:log-manager:flush-log-file"); + + await activateMenuItem(); + await promiseObserved; + ok(true, "Got flush-log-file observer message"); + + await closeConfirmationHint(); + sandbox.restore(); +}); + +async function checkForConfirmationHint(targetId) { + const sandbox = setupSendTabMocks({ fxaDevices }); + updateTabContextMenu(testTab); + + await openTabContextMenu("context_sendTabToDevice"); + await activateMenuItem(); + is( + ConfirmationHint._panel.anchorNode.id, + targetId, + `Hint anchored to ${targetId}` + ); + await closeConfirmationHint(); + sandbox.restore(); +} + +add_task(async function test_sendTabToDevice_showsConfirmationHint_fxa() { + // We need to change the fxastatus from "not_configured" to show the FxA button. + is( + document.documentElement.getAttribute("fxastatus"), + "not_configured", + "FxA button is hidden" + ); + document.documentElement.setAttribute("fxastatus", "foo"); + await checkForConfirmationHint("fxa-toolbar-menu-button"); + document.documentElement.setAttribute("fxastatus", "not_configured"); +}); + +add_task( + async function test_sendTabToDevice_showsConfirmationHint_onOverflowMenu() { + // We need to change the fxastatus from "not_configured" to show the FxA button. + is( + document.documentElement.getAttribute("fxastatus"), + "not_configured", + "FxA button is hidden" + ); + document.documentElement.setAttribute("fxastatus", "foo"); + + let navbar = document.getElementById("nav-bar"); + + // Resize the window so that the account button is in the overflow menu. + let originalWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + + await checkForConfirmationHint("PanelUI-menu-button"); + document.documentElement.setAttribute("fxastatus", "not_configured"); + + window.resizeTo(originalWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + CustomizableUI.reset(); + } +); + +add_task(async function test_sendTabToDevice_showsConfirmationHint_appMenu() { + // If fxastatus is "not_configured" then the FxA button is hidden, and we + // should use the appMenu. + is( + document.documentElement.getAttribute("fxastatus"), + "not_configured", + "FxA button is hidden" + ); + await checkForConfirmationHint("PanelUI-menu-button"); +}); + +add_task(async function test_tab_contextmenu() { + const sandbox = setupSendTabMocks({ fxaDevices }); + let expectation = sandbox + .mock(gSync) + .expects("sendTabToDevice") + .once() + .withExactArgs( + "about:mozilla", + [fxaDevices[1]], + "The Book of Mozilla, 6:27" + ) + .returns(true); + + updateTabContextMenu(testTab); + await openTabContextMenu("context_sendTabToDevice"); + is( + document.getElementById("context_sendTabToDevice").hidden, + false, + "Send tab to device is shown" + ); + is( + document.getElementById("context_sendTabToDevice").disabled, + false, + "Send tab to device is enabled" + ); + + await activateMenuItem(); + await closeConfirmationHint(); + + expectation.verify(); + sandbox.restore(); +}); + +add_task(async function test_tab_contextmenu_unconfigured() { + const sandbox = setupSendTabMocks({ state: UIState.STATUS_NOT_CONFIGURED }); + + updateTabContextMenu(testTab); + is( + document.getElementById("context_sendTabToDevice").hidden, + true, + "Send tab to device is hidden" + ); + is( + document.getElementById("context_sendTabToDevice").disabled, + false, + "Send tab to device is enabled" + ); + + sandbox.restore(); +}); + +add_task(async function test_tab_contextmenu_not_sendable() { + const sandbox = setupSendTabMocks({ fxaDevices, isSendableURI: false }); + + updateTabContextMenu(testTab); + is( + document.getElementById("context_sendTabToDevice").hidden, + true, + "Send tab to device is hidden" + ); + is( + document.getElementById("context_sendTabToDevice").disabled, + true, + "Send tab to device is disabled" + ); + + sandbox.restore(); +}); + +add_task(async function test_tab_contextmenu_not_synced_yet() { + const sandbox = setupSendTabMocks({ fxaDevices: null }); + + updateTabContextMenu(testTab); + is( + document.getElementById("context_sendTabToDevice").hidden, + true, + "Send tab to device is hidden" + ); + is( + document.getElementById("context_sendTabToDevice").disabled, + true, + "Send tab to device is disabled" + ); + + sandbox.restore(); +}); + +add_task(async function test_tab_contextmenu_sync_not_ready_configured() { + const sandbox = setupSendTabMocks({ syncReady: false }); + + updateTabContextMenu(testTab); + is( + document.getElementById("context_sendTabToDevice").hidden, + true, + "Send tab to device is hidden" + ); + is( + document.getElementById("context_sendTabToDevice").disabled, + true, + "Send tab to device is disabled" + ); + + sandbox.restore(); +}); + +add_task(async function test_tab_contextmenu_sync_not_ready_other_state() { + const sandbox = setupSendTabMocks({ + syncReady: false, + state: UIState.STATUS_NOT_VERIFIED, + }); + + updateTabContextMenu(testTab); + is( + document.getElementById("context_sendTabToDevice").hidden, + true, + "Send tab to device is hidden" + ); + is( + document.getElementById("context_sendTabToDevice").disabled, + false, + "Send tab to device is enabled" + ); + + sandbox.restore(); +}); + +add_task(async function test_tab_contextmenu_fxa_disabled() { + const getter = sinon.stub(gSync, "FXA_ENABLED").get(() => false); + // Simulate onFxaDisabled() being called on window open. + gSync.onFxaDisabled(); + + updateTabContextMenu(testTab); + is( + document.getElementById("context_sendTabToDevice").hidden, + true, + "Send tab to device is hidden" + ); + + getter.restore(); + [...document.querySelectorAll(".sync-ui-item")].forEach( + e => (e.hidden = false) + ); +}); + +add_task(async function teardown() { + Weave.Service.clientsEngine.getClientByFxaDeviceId.restore(); + Weave.Service.clientsEngine.getClientType.restore(); +}); + +async function openTabContextMenu(openSubmenuId = null) { + const contextMenu = document.getElementById("tabContextMenu"); + is(contextMenu.state, "closed", "checking if popup is closed"); + + const awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, { + type: "contextmenu", + button: 2, + }); + await awaitPopupShown; + + if (openSubmenuId) { + const menuPopup = document.getElementById(openSubmenuId).menupopup; + const menuPopupPromise = BrowserTestUtils.waitForEvent( + menuPopup, + "popupshown" + ); + menuPopup.openPopup(); + await menuPopupPromise; + } +} + +function promiseObserver(topic) { + return new Promise(resolve => { + let obs = (aSubject, aTopic, aData) => { + Services.obs.removeObserver(obs, aTopic); + resolve(aSubject); + }; + Services.obs.addObserver(obs, topic); + }); +} + +function waitForConfirmationHint() { + return BrowserTestUtils.waitForEvent(ConfirmationHint._panel, "popuphidden"); +} + +async function activateMenuItem() { + let popupHidden = BrowserTestUtils.waitForEvent( + document.getElementById("tabContextMenu"), + "popuphidden" + ); + let hintShown = BrowserTestUtils.waitForEvent( + ConfirmationHint._panel, + "popupshown" + ); + let menuitem = document + .getElementById("context_sendTabToDevicePopupMenu") + .querySelector("menuitem"); + menuitem.closest("menupopup").activateItem(menuitem); + await popupHidden; + await hintShown; +} + +async function closeConfirmationHint() { + let hintHidden = BrowserTestUtils.waitForEvent( + ConfirmationHint._panel, + "popuphidden" + ); + ConfirmationHint._panel.hidePopup(); + await hintHidden; +} diff --git a/browser/base/content/test/sync/browser_fxa_badge.js b/browser/base/content/test/sync/browser_fxa_badge.js new file mode 100644 index 0000000000..227d778d6c --- /dev/null +++ b/browser/base/content/test/sync/browser_fxa_badge.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +add_task(async function test_unconfigured_no_badge() { + const oldUIState = UIState.get; + + UIState.get = () => ({ + status: UIState.STATUS_NOT_CONFIGURED, + }); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + checkFxABadge(false); + + UIState.get = oldUIState; +}); + +add_task(async function test_signedin_no_badge() { + const oldUIState = UIState.get; + + UIState.get = () => ({ + status: UIState.STATUS_SIGNED_IN, + lastSync: new Date(), + email: "foo@bar.com", + }); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + checkFxABadge(false); + + UIState.get = oldUIState; +}); + +add_task(async function test_unverified_badge_shown() { + const oldUIState = UIState.get; + + UIState.get = () => ({ + status: UIState.STATUS_NOT_VERIFIED, + email: "foo@bar.com", + }); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + checkFxABadge(true); + + UIState.get = oldUIState; +}); + +add_task(async function test_loginFailed_badge_shown() { + const oldUIState = UIState.get; + + UIState.get = () => ({ + status: UIState.STATUS_LOGIN_FAILED, + email: "foo@bar.com", + }); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + checkFxABadge(true); + + UIState.get = oldUIState; +}); + +function checkFxABadge(shouldBeShown) { + let fxaButton = document.getElementById("fxa-toolbar-menu-button"); + let isShown = + fxaButton.hasAttribute("badge-status") || + fxaButton + .querySelector(".toolbarbutton-badge") + .classList.contains("feature-callout"); + is(isShown, shouldBeShown, "Fxa badge shown matches expected value."); +} diff --git a/browser/base/content/test/sync/browser_fxa_web_channel.html b/browser/base/content/test/sync/browser_fxa_web_channel.html new file mode 100644 index 0000000000..927b3523e9 --- /dev/null +++ b/browser/base/content/test/sync/browser_fxa_web_channel.html @@ -0,0 +1,158 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>fxa_web_channel_test</title> +</head> +<body> +<script> + var webChannelId = "account_updates_test"; + + window.onload = function() { + var testName = window.location.search.replace(/^\?/, ""); + + switch (testName) { + case "profile_change": + test_profile_change(); + break; + case "login": + test_login(); + break; + case "can_link_account": + test_can_link_account(); + break; + case "logout": + test_logout(); + break; + case "delete": + test_delete(); + break; + case "firefox_view": + test_firefox_view(); + break; + } + }; + + function test_profile_change() { + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: webChannelId, + message: { + command: "profile:change", + data: { + uid: "abc123", + }, + }, + }), + }); + + window.dispatchEvent(event); + } + + function test_login() { + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: webChannelId, + message: { + command: "fxaccounts:login", + data: { + authAt: Date.now(), + email: "testuser@testuser.com", + keyFetchToken: "key_fetch_token", + sessionToken: "session_token", + uid: "uid", + unwrapBKey: "unwrap_b_key", + verified: true, + }, + messageId: 1, + }, + }), + }); + + window.dispatchEvent(event); + } + + function test_can_link_account() { + window.addEventListener("WebChannelMessageToContent", function(e) { + // echo any responses from the browser back to the tests on the + // fxaccounts_webchannel_response_echo WebChannel. The tests are + // listening for events and do the appropriate checks. + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: "fxaccounts_webchannel_response_echo", + message: e.detail.message, + }), + }); + + window.dispatchEvent(event); + }, true); + + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: webChannelId, + message: { + command: "fxaccounts:can_link_account", + data: { + email: "testuser@testuser.com", + }, + messageId: 2, + }, + }), + }); + + window.dispatchEvent(event); + } + + function test_logout() { + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: webChannelId, + message: { + command: "fxaccounts:logout", + data: { + uid: "uid", + }, + messageId: 3, + }, + }), + }); + + window.dispatchEvent(event); + } + + function test_delete() { + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: webChannelId, + message: { + command: "fxaccounts:delete", + data: { + uid: "uid", + }, + messageId: 4, + }, + }), + }); + + window.dispatchEvent(event); + } + + function test_firefox_view() { + var event = new window.CustomEvent("WebChannelMessageToChrome", { + detail: JSON.stringify({ + id: webChannelId, + message: { + command: "fxaccounts:firefox_view", + data: { + uid: "uid", + }, + messageId: 5, + }, + }), + }); + + window.dispatchEvent(event); + } +</script> +</body> +</html> diff --git a/browser/base/content/test/sync/browser_fxa_web_channel.js b/browser/base/content/test/sync/browser_fxa_web_channel.js new file mode 100644 index 0000000000..c232f26f26 --- /dev/null +++ b/browser/base/content/test/sync/browser_fxa_web_channel.js @@ -0,0 +1,280 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +ChromeUtils.defineESModuleGetters(this, { + WebChannel: "resource://gre/modules/WebChannel.sys.mjs", + ON_PROFILE_CHANGE_NOTIFICATION: + "resource://gre/modules/FxAccountsCommon.sys.mjs", +}); + +var { FxAccountsWebChannel } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsWebChannel.sys.mjs" +); + +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const TEST_HTTP_PATH = "http://example.com"; +const TEST_BASE_URL = + TEST_HTTP_PATH + + "/browser/browser/base/content/test/sync/browser_fxa_web_channel.html"; +const TEST_CHANNEL_ID = "account_updates_test"; + +var gTests = [ + { + desc: "FxA Web Channel - should receive message about profile changes", + async run() { + let client = new FxAccountsWebChannel({ + content_uri: TEST_HTTP_PATH, + channel_id: TEST_CHANNEL_ID, + }); + let promiseObserver = new Promise((resolve, reject) => { + makeObserver( + ON_PROFILE_CHANGE_NOTIFICATION, + function (subject, topic, data) { + Assert.equal(data, "abc123"); + client.tearDown(); + resolve(); + } + ); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_BASE_URL + "?profile_change", + }, + async function () { + await promiseObserver; + } + ); + }, + }, + { + desc: "fxa web channel - login messages should notify the fxAccounts object", + async run() { + let promiseLogin = new Promise((resolve, reject) => { + let login = accountData => { + Assert.equal(typeof accountData.authAt, "number"); + Assert.equal(accountData.email, "testuser@testuser.com"); + Assert.equal(accountData.keyFetchToken, "key_fetch_token"); + Assert.equal(accountData.sessionToken, "session_token"); + Assert.equal(accountData.uid, "uid"); + Assert.equal(accountData.unwrapBKey, "unwrap_b_key"); + Assert.equal(accountData.verified, true); + + client.tearDown(); + resolve(); + }; + + let client = new FxAccountsWebChannel({ + content_uri: TEST_HTTP_PATH, + channel_id: TEST_CHANNEL_ID, + helpers: { + login, + }, + }); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_BASE_URL + "?login", + }, + async function () { + await promiseLogin; + } + ); + }, + }, + { + desc: "fxa web channel - can_link_account messages should respond", + async run() { + let properUrl = TEST_BASE_URL + "?can_link_account"; + + let promiseEcho = new Promise((resolve, reject) => { + let webChannelOrigin = Services.io.newURI(properUrl); + // responses sent to content are echoed back over the + // `fxaccounts_webchannel_response_echo` channel. Ensure the + // fxaccounts:can_link_account message is responded to. + let echoWebChannel = new WebChannel( + "fxaccounts_webchannel_response_echo", + webChannelOrigin + ); + echoWebChannel.listen((webChannelId, message, target) => { + Assert.equal(message.command, "fxaccounts:can_link_account"); + Assert.equal(message.messageId, 2); + Assert.equal(message.data.ok, true); + + client.tearDown(); + echoWebChannel.stopListening(); + + resolve(); + }); + + let client = new FxAccountsWebChannel({ + content_uri: TEST_HTTP_PATH, + channel_id: TEST_CHANNEL_ID, + helpers: { + shouldAllowRelink(acctName) { + return acctName === "testuser@testuser.com"; + }, + }, + }); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: properUrl, + }, + async function () { + await promiseEcho; + } + ); + }, + }, + { + desc: "fxa web channel - logout messages should notify the fxAccounts object", + async run() { + let promiseLogout = new Promise((resolve, reject) => { + let logout = uid => { + Assert.equal(uid, "uid"); + + client.tearDown(); + resolve(); + }; + + let client = new FxAccountsWebChannel({ + content_uri: TEST_HTTP_PATH, + channel_id: TEST_CHANNEL_ID, + helpers: { + logout, + }, + }); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_BASE_URL + "?logout", + }, + async function () { + await promiseLogout; + } + ); + }, + }, + { + desc: "fxa web channel - delete messages should notify the fxAccounts object", + async run() { + let promiseDelete = new Promise((resolve, reject) => { + let logout = uid => { + Assert.equal(uid, "uid"); + + client.tearDown(); + resolve(); + }; + + let client = new FxAccountsWebChannel({ + content_uri: TEST_HTTP_PATH, + channel_id: TEST_CHANNEL_ID, + helpers: { + logout, + }, + }); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_BASE_URL + "?delete", + }, + async function () { + await promiseDelete; + } + ); + }, + }, + { + desc: "fxa web channel - firefox_view messages should call the openFirefoxView helper", + async run() { + let wasCalled = false; + let promiseMessageHandled = new Promise((resolve, reject) => { + let openFirefoxView = (browser, entryPoint) => { + wasCalled = true; + Assert.ok( + !!browser.ownerGlobal, + "openFirefoxView called with a browser argument" + ); + Assert.equal( + typeof browser.ownerGlobal.FirefoxViewHandler.openTab, + "function", + "We can reach the openTab method" + ); + + client.tearDown(); + resolve(); + }; + + let client = new FxAccountsWebChannel({ + content_uri: TEST_HTTP_PATH, + channel_id: TEST_CHANNEL_ID, + helpers: { + openFirefoxView, + }, + }); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_BASE_URL + "?firefox_view", + }, + async function () { + await promiseMessageHandled; + } + ); + Assert.ok(wasCalled, "openFirefoxView did get called"); + }, + }, +]; // gTests + +function makeObserver(aObserveTopic, aObserveFunc) { + let callback = function (aSubject, aTopic, aData) { + if (aTopic == aObserveTopic) { + removeMe(); + aObserveFunc(aSubject, aTopic, aData); + } + }; + + function removeMe() { + Services.obs.removeObserver(callback, aObserveTopic); + } + + Services.obs.addObserver(callback, aObserveTopic); + return removeMe; +} + +registerCleanupFunction(function () { + Services.prefs.clearUserPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess" + ); +}); + +function test() { + waitForExplicitFinish(); + Services.prefs.setBoolPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", + false + ); + + (async function () { + for (let testCase of gTests) { + info("Running: " + testCase.desc); + await testCase.run(); + } + })().then(finish, ex => { + Assert.ok(false, "Unexpected Exception: " + ex); + finish(); + }); +} diff --git a/browser/base/content/test/sync/browser_sync.js b/browser/base/content/test/sync/browser_sync.js new file mode 100644 index 0000000000..168c6f22b0 --- /dev/null +++ b/browser/base/content/test/sync/browser_sync.js @@ -0,0 +1,935 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); + +let gCUITestUtils = new CustomizableUITestUtils(window); + +add_setup(async function () { + // gSync.init() is called in a requestIdleCallback. Force its initialization. + gSync.init(); + // This preference gets set the very first time that the FxA menu gets opened, + // which can cause a state write to occur, which can confuse this test, since + // when in the signed-out state, we need to set the state _before_ opening + // the FxA menu (since the panel cannot be opened) in the signed out state. + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.toolbar.accessed", true]], + }); +}); + +add_task(async function test_ui_state_notification_calls_updateAllUI() { + let called = false; + let updateAllUI = gSync.updateAllUI; + gSync.updateAllUI = () => { + called = true; + }; + + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + ok(called); + + gSync.updateAllUI = updateAllUI; +}); + +add_task(async function test_navBar_button_visibility() { + const button = document.getElementById("fxa-toolbar-menu-button"); + ok(button.closest("#nav-bar"), "button is in the #nav-bar"); + + const state = { + status: UIState.STATUS_NOT_CONFIGURED, + syncEnabled: true, + }; + gSync.updateAllUI(state); + ok( + BrowserTestUtils.isVisible(button), + "Check button visibility with STATUS_NOT_CONFIGURED" + ); + + state.email = "foo@bar.com"; + state.status = UIState.STATUS_NOT_VERIFIED; + gSync.updateAllUI(state); + ok( + BrowserTestUtils.isVisible(button), + "Check button visibility with STATUS_NOT_VERIFIED" + ); + + state.status = UIState.STATUS_LOGIN_FAILED; + gSync.updateAllUI(state); + ok( + BrowserTestUtils.isVisible(button), + "Check button visibility with STATUS_LOGIN_FAILED" + ); + + state.status = UIState.STATUS_SIGNED_IN; + gSync.updateAllUI(state); + ok( + BrowserTestUtils.isVisible(button), + "Check button visibility with STATUS_SIGNED_IN" + ); + + state.syncEnabled = false; + gSync.updateAllUI(state); + is( + BrowserTestUtils.isVisible(button), + true, + "Check button visibility when signed in, but sync disabled" + ); +}); + +add_task(async function test_overflow_navBar_button_visibility() { + const button = document.getElementById("fxa-toolbar-menu-button"); + + let overflowPanel = document.getElementById("widget-overflow"); + overflowPanel.setAttribute("animate", "false"); + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + let originalWindowWidth = window.outerWidth; + + registerCleanupFunction(function () { + overflowPanel.removeAttribute("animate"); + window.resizeTo(originalWindowWidth, window.outerHeight); + return TestUtils.waitForCondition( + () => !navbar.hasAttribute("overflowing") + ); + }); + + window.resizeTo(450, window.outerHeight); + + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let chevron = document.getElementById("nav-bar-overflow-button"); + let shownPanelPromise = BrowserTestUtils.waitForEvent( + overflowPanel, + "popupshown" + ); + chevron.click(); + await shownPanelPromise; + + ok(button, "fxa-toolbar-menu-button was found"); + + const state = { + status: UIState.STATUS_NOT_CONFIGURED, + syncEnabled: true, + }; + gSync.updateAllUI(state); + + ok( + BrowserTestUtils.isVisible(button), + "Button should still be visable even if user sync not configured" + ); + + let hidePanelPromise = BrowserTestUtils.waitForEvent( + overflowPanel, + "popuphidden" + ); + chevron.click(); + await hidePanelPromise; +}); + +add_task(async function setupForPanelTests() { + /* Proton hides the FxA toolbar button when in the nav-bar and unconfigured. + To test the panel in all states, we move it to the tabstrip toolbar where + it will always be visible. + */ + CustomizableUI.addWidgetToArea( + "fxa-toolbar-menu-button", + CustomizableUI.AREA_TABSTRIP + ); + + // make sure it gets put back at the end of the tests + registerCleanupFunction(() => { + CustomizableUI.addWidgetToArea( + "fxa-toolbar-menu-button", + CustomizableUI.AREA_NAVBAR + ); + }); +}); + +add_task(async function test_ui_state_signedin() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + const relativeDateAnchor = new Date(); + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + lastSync: new Date(), + syncing: false, + }; + + const origRelativeTimeFormat = gSync.relativeTimeFormat; + gSync.relativeTimeFormat = { + formatBestUnit(date) { + return origRelativeTimeFormat.formatBestUnit(date, { + now: relativeDateAnchor, + }); + }, + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + checkMenuBarItem("sync-syncnowitem"); + checkPanelHeader(); + ok( + BrowserTestUtils.isVisible( + document.getElementById("fxa-menu-header-title") + ), + "expected toolbar to be visible after opening" + ); + checkFxaToolbarButtonPanel({ + headerTitle: "Manage account", + headerDescription: state.displayName, + enabledItems: [ + "PanelUI-fxa-menu-sendtab-button", + "PanelUI-fxa-menu-connect-device-button", + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + "PanelUI-fxa-menu-account-signout-button", + ], + disabledItems: [], + hiddenItems: ["PanelUI-fxa-menu-setup-sync-button"], + }); + checkFxAAvatar("signedin"); + gSync.relativeTimeFormat = origRelativeTimeFormat; + await closeFxaPanel(); + + await openMainPanel(); + + checkPanelUIStatusBar({ + description: "Foo Bar", + titleHidden: true, + hideFxAText: true, + }); + + await closeTabAndMainPanel(); +}); + +add_task(async function test_ui_state_syncing_panel_closed() { + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + lastSync: new Date(), + syncing: true, + }; + + gSync.updateAllUI(state); + + checkSyncNowButtons(true); + + // Be good citizens and remove the "syncing" state. + gSync.updateAllUI({ + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + lastSync: new Date(), + syncing: false, + }); + // Because we switch from syncing to non-syncing, and there's a timeout involved. + await promiseObserver("test:browser-sync:activity-stop"); +}); + +add_task(async function test_ui_state_syncing_panel_open() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + lastSync: new Date(), + syncing: false, + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + checkSyncNowButtons(false); + + state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + lastSync: new Date(), + syncing: true, + }; + + gSync.updateAllUI(state); + + checkSyncNowButtons(true); + + // Be good citizens and remove the "syncing" state. + gSync.updateAllUI({ + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + lastSync: new Date(), + syncing: false, + }); + // Because we switch from syncing to non-syncing, and there's a timeout involved. + await promiseObserver("test:browser-sync:activity-stop"); + + await closeFxaPanel(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_ui_state_panel_open_after_syncing() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + lastSync: new Date(), + syncing: true, + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + checkSyncNowButtons(true); + + // Be good citizens and remove the "syncing" state. + gSync.updateAllUI({ + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + lastSync: new Date(), + syncing: false, + }); + // Because we switch from syncing to non-syncing, and there's a timeout involved. + await promiseObserver("test:browser-sync:activity-stop"); + + await closeFxaPanel(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_ui_state_unconfigured() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_NOT_CONFIGURED, + }; + + gSync.updateAllUI(state); + + checkMenuBarItem("sync-setup"); + + checkFxAAvatar("not_configured"); + + let signedOffLabel = gSync.fluentStrings.formatValueSync( + "appmenu-fxa-signed-in-label" + ); + + await openMainPanel(); + + checkPanelUIStatusBar({ + description: signedOffLabel, + titleHidden: true, + hideFxAText: false, + }); + await closeTabAndMainPanel(); +}); + +add_task(async function test_ui_state_signed_in() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: false, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + checkMenuBarItem("sync-enable"); + checkPanelHeader(); + checkFxaToolbarButtonPanel({ + headerTitle: "Manage account", + headerDescription: "Foo Bar", + enabledItems: [ + "PanelUI-fxa-menu-sendtab-button", + "PanelUI-fxa-menu-connect-device-button", + "PanelUI-fxa-menu-setup-sync-button", + "PanelUI-fxa-menu-account-signout-button", + ], + disabledItems: [], + hiddenItems: [ + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + ], + }); + checkFxAAvatar("signedin"); + await closeFxaPanel(); + + await openMainPanel(); + + checkPanelUIStatusBar({ + description: "Foo Bar", + titleHidden: true, + hideFxAText: true, + }); + + await closeTabAndMainPanel(); +}); + +add_task(async function test_ui_state_signed_in_no_display_name() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: false, + email: "foo@bar.com", + avatarURL: "https://foo.bar", + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + checkMenuBarItem("sync-enable"); + checkPanelHeader(); + checkFxaToolbarButtonPanel({ + headerTitle: "Manage account", + headerDescription: "foo@bar.com", + enabledItems: [ + "PanelUI-fxa-menu-sendtab-button", + "PanelUI-fxa-menu-connect-device-button", + "PanelUI-fxa-menu-setup-sync-button", + "PanelUI-fxa-menu-account-signout-button", + ], + disabledItems: [], + hiddenItems: [ + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + ], + }); + checkFxAAvatar("signedin"); + await closeFxaPanel(); + + await openMainPanel(); + + checkPanelUIStatusBar({ + description: "foo@bar.com", + titleHidden: true, + hideFxAText: true, + }); + + await closeTabAndMainPanel(); +}); + +add_task(async function test_ui_state_unverified() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_NOT_VERIFIED, + email: "foo@bar.com", + syncing: false, + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + const expectedLabel = gSync.fluentStrings.formatValueSync( + "account-finish-account-setup" + ); + + checkMenuBarItem("sync-unverifieditem"); + checkPanelHeader(); + checkFxaToolbarButtonPanel({ + headerTitle: expectedLabel, + headerDescription: state.email, + enabledItems: [ + "PanelUI-fxa-menu-sendtab-button", + "PanelUI-fxa-menu-setup-sync-button", + "PanelUI-fxa-menu-account-signout-button", + ], + disabledItems: ["PanelUI-fxa-menu-connect-device-button"], + hiddenItems: [ + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + ], + }); + checkFxAAvatar("unverified"); + await closeFxaPanel(); + await openMainPanel(); + + checkPanelUIStatusBar({ + description: state.email, + title: expectedLabel, + titleHidden: false, + hideFxAText: true, + }); + + await closeTabAndMainPanel(); +}); + +add_task(async function test_ui_state_loginFailed() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + let state = { + status: UIState.STATUS_LOGIN_FAILED, + email: "foo@bar.com", + displayName: "Foo Bar", + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + const expectedLabel = gSync.fluentStrings.formatValueSync( + "account-disconnected2" + ); + + checkMenuBarItem("sync-reauthitem"); + checkPanelHeader(); + checkFxaToolbarButtonPanel({ + headerTitle: expectedLabel, + headerDescription: state.displayName, + enabledItems: [ + "PanelUI-fxa-menu-sendtab-button", + "PanelUI-fxa-menu-setup-sync-button", + "PanelUI-fxa-menu-account-signout-button", + ], + disabledItems: ["PanelUI-fxa-menu-connect-device-button"], + hiddenItems: [ + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + ], + }); + checkFxAAvatar("login-failed"); + await closeFxaPanel(); + await openMainPanel(); + + checkPanelUIStatusBar({ + description: state.displayName, + title: expectedLabel, + titleHidden: false, + hideFxAText: true, + }); + + await closeTabAndMainPanel(); +}); + +add_task(async function test_app_menu_fxa_disabled() { + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + + Services.prefs.setBoolPref("identity.fxaccounts.enabled", true); + newWin.gSync.onFxaDisabled(); + + let menuButton = newWin.document.getElementById("PanelUI-menu-button"); + menuButton.click(); + await BrowserTestUtils.waitForEvent(newWin.PanelUI.mainView, "ViewShown"); + + [...newWin.document.querySelectorAll(".sync-ui-item")].forEach( + e => (e.hidden = false) + ); + + let hidden = BrowserTestUtils.waitForEvent( + newWin.document, + "popuphidden", + true + ); + newWin.PanelUI.hide(); + await hidden; + await BrowserTestUtils.closeWindow(newWin); +}); + +add_task( + // Can't open the history menu in tests on Mac. + () => AppConstants.platform != "mac", + async function test_history_menu_fxa_disabled() { + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + + Services.prefs.setBoolPref("identity.fxaccounts.enabled", true); + newWin.gSync.onFxaDisabled(); + + const historyMenubarItem = window.document.getElementById("history-menu"); + const historyMenu = window.document.getElementById("historyMenuPopup"); + const syncedTabsItem = historyMenu.querySelector("#sync-tabs-menuitem"); + const menuShown = BrowserTestUtils.waitForEvent(historyMenu, "popupshown"); + historyMenubarItem.openMenu(true); + await menuShown; + + Assert.equal( + syncedTabsItem.hidden, + true, + "Synced Tabs item should not be displayed when FxAccounts is disabled" + ); + const menuHidden = BrowserTestUtils.waitForEvent( + historyMenu, + "popuphidden" + ); + historyMenu.hidePopup(); + await menuHidden; + await BrowserTestUtils.closeWindow(newWin); + } +); + +// If the PXI experiment is enabled, we need to ensure we can see the CTAs when signed out +add_task(async function test_experiment_ui_state_unconfigured() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + // The experiment enables this bool, found in FeatureManifest.yaml + Services.prefs.setBoolPref( + "identity.fxaccounts.toolbar.pxiToolbarEnabled", + true + ); + let state = { + status: UIState.STATUS_NOT_CONFIGURED, + }; + + gSync.updateAllUI(state); + + checkMenuBarItem("sync-setup"); + + checkFxAAvatar("not_configured"); + + let expectedLabel = gSync.fluentStrings.formatValueSync( + "appmenuitem-sign-in-account" + ); + + await openMainPanel(); + + checkFxaToolbarButtonPanel({ + headerTitle: expectedLabel, + headerDescription: "", + enabledItems: [ + "PanelUI-fxa-cta-menu", + "PanelUI-fxa-menu-sync-button", + "PanelUI-fxa-menu-monitor-button", + "PanelUI-fxa-menu-relay-button", + "PanelUI-fxa-menu-vpn-button", + ], + disabledItems: [], + hiddenItems: [ + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + ], + }); + + // Revert the pref at the end of the test + Services.prefs.setBoolPref( + "identity.fxaccounts.toolbar.pxiToolbarEnabled", + false + ); + await closeTabAndMainPanel(); +}); + +// Ensure we can see the regular signed in flow + the extra PXI CTAs when +// the experiment is enabled +add_task(async function test_experiment_ui_state_signedin() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + + // The experiment enables this bool, found in FeatureManifest.yaml + Services.prefs.setBoolPref( + "identity.fxaccounts.toolbar.pxiToolbarEnabled", + true + ); + + const relativeDateAnchor = new Date(); + let state = { + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + displayName: "Foo Bar", + avatarURL: "https://foo.bar", + lastSync: new Date(), + syncing: false, + }; + + const origRelativeTimeFormat = gSync.relativeTimeFormat; + gSync.relativeTimeFormat = { + formatBestUnit(date) { + return origRelativeTimeFormat.formatBestUnit(date, { + now: relativeDateAnchor, + }); + }, + }; + + gSync.updateAllUI(state); + + await openFxaPanel(); + + checkMenuBarItem("sync-syncnowitem"); + checkPanelHeader(); + ok( + BrowserTestUtils.isVisible( + document.getElementById("fxa-menu-header-title") + ), + "expected toolbar to be visible after opening" + ); + checkFxaToolbarButtonPanel({ + headerTitle: "Manage account", + headerDescription: state.displayName, + enabledItems: [ + "PanelUI-fxa-menu-sendtab-button", + "PanelUI-fxa-menu-connect-device-button", + "PanelUI-fxa-menu-syncnow-button", + "PanelUI-fxa-menu-sync-prefs-button", + "PanelUI-fxa-menu-account-signout-button", + "PanelUI-fxa-cta-menu", + "PanelUI-fxa-menu-sync-button", + "PanelUI-fxa-menu-monitor-button", + "PanelUI-fxa-menu-relay-button", + "PanelUI-fxa-menu-vpn-button", + ], + disabledItems: [], + hiddenItems: ["PanelUI-fxa-menu-setup-sync-button"], + }); + checkFxAAvatar("signedin"); + gSync.relativeTimeFormat = origRelativeTimeFormat; + await closeFxaPanel(); + + await openMainPanel(); + + checkPanelUIStatusBar({ + description: "Foo Bar", + titleHidden: true, + hideFxAText: true, + }); + + // Revert the pref at the end of the test + Services.prefs.setBoolPref( + "identity.fxaccounts.toolbar.pxiToolbarEnabled", + false + ); + await closeTabAndMainPanel(); +}); + +function checkPanelUIStatusBar({ + description, + title, + titleHidden, + hideFxAText, +}) { + checkAppMenuFxAText(hideFxAText); + let appMenuHeaderTitle = PanelMultiView.getViewNode( + document, + "appMenu-header-title" + ); + let appMenuHeaderDescription = PanelMultiView.getViewNode( + document, + "appMenu-header-description" + ); + is( + appMenuHeaderDescription.value, + description, + "app menu description has correct value" + ); + is(appMenuHeaderTitle.hidden, titleHidden, "title has correct hidden status"); + if (!titleHidden) { + is(appMenuHeaderTitle.value, title, "title has correct value"); + } +} + +function checkMenuBarItem(expectedShownItemId) { + checkItemsVisibilities( + [ + "sync-setup", + "sync-enable", + "sync-syncnowitem", + "sync-reauthitem", + "sync-unverifieditem", + ], + expectedShownItemId + ); +} + +function checkPanelHeader() { + let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa"); + is( + fxaPanelView.getAttribute("title"), + gSync.fluentStrings.formatValueSync("appmenu-account-header"), + "Panel title is correct" + ); +} + +function checkSyncNowButtons(syncing, tooltip = null) { + const syncButtons = document.querySelectorAll(".syncNowBtn"); + + for (const syncButton of syncButtons) { + is( + syncButton.getAttribute("syncstatus"), + syncing ? "active" : "", + "button active has the right value" + ); + if (tooltip) { + is( + syncButton.getAttribute("tooltiptext"), + tooltip, + "button tooltiptext is set to the right value" + ); + } + } + + const syncLabels = document.querySelectorAll(".syncnow-label"); + + for (const syncLabel of syncLabels) { + if (syncing) { + is( + syncLabel.getAttribute("data-l10n-id"), + syncLabel.getAttribute("syncing-data-l10n-id"), + "label is set to the right value" + ); + } else { + is( + syncLabel.getAttribute("data-l10n-id"), + syncLabel.getAttribute("sync-now-data-l10n-id"), + "label is set to the right value" + ); + } + } +} + +async function checkFxaToolbarButtonPanel({ + headerTitle, + headerDescription, + enabledItems, + disabledItems, + hiddenItems, +}) { + is( + document.getElementById("fxa-menu-header-title").value, + headerTitle, + "has correct title" + ); + is( + document.getElementById("fxa-menu-header-description").value, + headerDescription, + "has correct description" + ); + + for (const id of enabledItems) { + const el = document.getElementById(id); + is(el.hasAttribute("disabled"), false, id + " is enabled"); + } + + for (const id of disabledItems) { + const el = document.getElementById(id); + is(el.getAttribute("disabled"), "true", id + " is disabled"); + } + + for (const id of hiddenItems) { + const el = document.getElementById(id); + is(el.getAttribute("hidden"), "true", id + " is hidden"); + } +} + +async function checkFxABadged() { + const button = document.getElementById("fxa-toolbar-menu-button"); + await BrowserTestUtils.waitForCondition(() => { + return button.querySelector("label.feature-callout"); + }); + const badge = button.querySelector("label.feature-callout"); + ok(badge, "expected feature-callout style badge"); + ok(BrowserTestUtils.isVisible(badge), "expected the badge to be visible"); +} + +// fxaStatus is one of 'not_configured', 'unverified', 'login-failed', or 'signedin'. +function checkFxAAvatar(fxaStatus) { + // Unhide the panel so computed styles can be read + document.querySelector("#appMenu-popup").hidden = false; + + const avatarContainers = [document.getElementById("fxa-avatar-image")]; + for (const avatar of avatarContainers) { + const avatarURL = getComputedStyle(avatar).listStyleImage; + const expected = { + not_configured: 'url("chrome://browser/skin/fxa/avatar-empty.svg")', + unverified: 'url("chrome://browser/skin/fxa/avatar.svg")', + signedin: 'url("chrome://browser/skin/fxa/avatar.svg")', + "login-failed": 'url("chrome://browser/skin/fxa/avatar.svg")', + }; + Assert.equal( + avatarURL, + expected[fxaStatus], + `expected avatar URL to be ${expected[fxaStatus]}, got ${avatarURL}` + ); + } +} + +function checkAppMenuFxAText(hideStatus) { + let fxaText = document.getElementById("appMenu-fxa-text"); + let isHidden = fxaText.hidden || fxaText.style.visibility == "collapse"; + Assert.equal(isHidden, hideStatus, "FxA text has correct hidden state"); +} + +// Only one item visible at a time. +function checkItemsVisibilities(itemsIds, expectedShownItemId) { + for (let id of itemsIds) { + if (id == expectedShownItemId) { + ok( + !document.getElementById(id).hidden, + "menuitem " + id + " should be visible" + ); + } else { + ok( + document.getElementById(id).hidden, + "menuitem " + id + " should be hidden" + ); + } + } +} + +function promiseObserver(topic) { + return new Promise(resolve => { + let obs = (aSubject, aTopic, aData) => { + Services.obs.removeObserver(obs, aTopic); + resolve(aSubject); + }; + Services.obs.addObserver(obs, topic); + }); +} + +async function openTabAndFxaPanel() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/"); + await openFxaPanel(); +} + +async function openFxaPanel() { + let fxaButton = document.getElementById("fxa-toolbar-menu-button"); + fxaButton.click(); + + let fxaView = PanelMultiView.getViewNode(document, "PanelUI-fxa"); + await BrowserTestUtils.waitForEvent(fxaView, "ViewShown"); +} + +async function closeFxaPanel() { + let fxaView = PanelMultiView.getViewNode(document, "PanelUI-fxa"); + let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true); + fxaView.closest("panel").hidePopup(); + await hidden; +} + +async function openMainPanel() { + let menuButton = document.getElementById("PanelUI-menu-button"); + menuButton.click(); + await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown"); +} + +async function closeTabAndMainPanel() { + await gCUITestUtils.hideMainMenu(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} diff --git a/browser/base/content/test/sync/browser_synced_tabs_view.js b/browser/base/content/test/sync/browser_synced_tabs_view.js new file mode 100644 index 0000000000..eb1203825e --- /dev/null +++ b/browser/base/content/test/sync/browser_synced_tabs_view.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function promiseLayout() { + // Wait for layout to have happened. + return new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)) + ); +} + +add_setup(async function () { + registerCleanupFunction(() => CustomizableUI.reset()); +}); + +async function withOpenSyncPanel(cb) { + let promise = BrowserTestUtils.waitForEvent( + window, + "ViewShown", + true, + e => e.target.id == "PanelUI-remotetabs" + ).then(e => e.target.closest("panel")); + + let panel; + try { + gSync.openSyncedTabsPanel(); + panel = await promise; + is(panel.state, "open", "Panel should have opened."); + await cb(panel); + } finally { + panel?.hidePopup(); + } +} + +add_task(async function test_button_in_bookmarks_toolbar() { + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_BOOKMARKS); + CustomizableUI.setToolbarVisibility(CustomizableUI.AREA_BOOKMARKS, "never"); + await promiseLayout(); + + await withOpenSyncPanel(async panel => { + is( + panel.anchorNode.closest("toolbarbutton"), + PanelUI.menuButton, + "Should have anchored on the menu button because the sync button isn't visible." + ); + }); +}); + +add_task(async function test_button_in_navbar() { + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_NAVBAR, 0); + await promiseLayout(); + await withOpenSyncPanel(async panel => { + is( + panel.anchorNode.closest("toolbarbutton").id, + "sync-button", + "Should have anchored on the sync button itself." + ); + }); +}); + +add_task(async function test_button_in_overflow() { + CustomizableUI.addWidgetToArea( + "sync-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + 0 + ); + await promiseLayout(); + await withOpenSyncPanel(async panel => { + is( + panel.anchorNode.closest("toolbarbutton").id, + "nav-bar-overflow-button", + "Should have anchored on the overflow button." + ); + }); +}); diff --git a/browser/base/content/test/sync/head.js b/browser/base/content/test/sync/head.js new file mode 100644 index 0000000000..10ffb2a2d2 --- /dev/null +++ b/browser/base/content/test/sync/head.js @@ -0,0 +1,34 @@ +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +function promiseSyncReady() { + let service = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + return service.whenLoaded(); +} + +function setupSendTabMocks({ + fxaDevices = null, + state = UIState.STATUS_SIGNED_IN, + isSendableURI = true, +}) { + const sandbox = sinon.createSandbox(); + sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices); + sandbox.stub(UIState, "get").returns({ + status: state, + syncEnabled: true, + }); + if (isSendableURI) { + sandbox.stub(BrowserUtils, "getShareableURL").returnsArg(0); + } else { + sandbox.stub(BrowserUtils, "getShareableURL").returns(null); + } + sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true); + sandbox.stub(fxAccounts.commands.sendTab, "send").resolves({ failed: [] }); + return sandbox; +} |