summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/sync
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /browser/base/content/test/sync
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--browser/base/content/test/sync/browser.ini13
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendpage.js465
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendtab.js362
-rw-r--r--browser/base/content/test/sync/browser_fxa_badge.js70
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.html158
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.js282
-rw-r--r--browser/base/content/test/sync/browser_sync.js751
-rw-r--r--browser/base/content/test/sync/browser_synced_tabs_view.js76
-rw-r--r--browser/base/content/test/sync/head.js34
9 files changed, 2211 insertions, 0 deletions
diff --git a/browser/base/content/test/sync/browser.ini b/browser/base/content/test/sync/browser.ini
new file mode 100644
index 0000000000..e7f6f889a0
--- /dev/null
+++ b/browser/base/content/test/sync/browser.ini
@@ -0,0 +1,13 @@
+[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..503d246f6a
--- /dev/null
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -0,0 +1,465 @@
+/* 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",
+ "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..903dd317ac
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
+ return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+});
+
+ChromeUtils.defineESModuleGetters(this, {
+ WebChannel: "resource://gre/modules/WebChannel.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(
+ FxAccountsCommon.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..a8354b7f10
--- /dev/null
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -0,0 +1,751 @@
+/* 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.is_hidden(button),
+ "Button should be hidden with STATUS_NOT_CONFIGURED"
+ );
+
+ state.email = "foo@bar.com";
+ state.status = UIState.STATUS_NOT_VERIFIED;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Check button visibility with STATUS_NOT_VERIFIED"
+ );
+
+ state.status = UIState.STATUS_LOGIN_FAILED;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Check button visibility with STATUS_LOGIN_FAILED"
+ );
+
+ state.status = UIState.STATUS_SIGNED_IN;
+ gSync.updateAllUI(state);
+ ok(
+ BrowserTestUtils.is_visible(button),
+ "Check button visibility with STATUS_SIGNED_IN"
+ );
+
+ state.syncEnabled = false;
+ gSync.updateAllUI(state);
+ is(
+ BrowserTestUtils.is_visible(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.is_hidden(button),
+ "Button should be hidden with STATUS_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();
+ checkFxaToolbarButtonPanel({
+ headerTitle: "Manage account",
+ headerDescription: "foo@bar.com",
+ 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.com",
+ 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_syncdisabled() {
+ 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.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",
+ };
+
+ gSync.updateAllUI(state);
+
+ await openFxaPanel();
+
+ const expectedLabel = gSync.fluentStrings.formatValueSync(
+ "account-disconnected2"
+ );
+
+ checkMenuBarItem("sync-reauthitem");
+ 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("login-failed");
+ await closeFxaPanel();
+ await openMainPanel();
+
+ checkPanelUIStatusBar({
+ description: state.email,
+ 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);
+ }
+);
+
+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-fxa-header2"),
+ "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.is_visible(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")',
+ };
+ ok(
+ 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";
+ ok(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;
+}