From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser/browser_sidebar_syncedtabslist.js | 646 +++++++++++++++++++++ 1 file changed, 646 insertions(+) create mode 100644 browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js (limited to 'browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js') diff --git a/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js new file mode 100644 index 0000000000..0874a50019 --- /dev/null +++ b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js @@ -0,0 +1,646 @@ +"use strict"; + +const { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +const FIXTURE = [ + { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: + "Firefox for Android — Mobile Web browser — More ways to customize and protect your privacy — Mozilla", + url: "https://www.mozilla.org/en-US/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=synced-tabs-sidebar", + icon: "chrome://global/skin/icons/defaultFavicon.svg", + client: "7cqCr77ptzX3", + lastUsed: 1452124677, + }, + ], + }, + { + id: "2xU5h-4bkWqA", + type: "client", + lastModified: 1492201200, + name: "laptop", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: + "Firefox for iOS — Mobile Web browser for your iPhone, iPad and iPod touch — Mozilla", + url: "https://www.mozilla.org/en-US/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=synced-tabs-sidebar", + icon: "moz-anno:favicon:https://www.mozilla.org/media/img/firefox/favicon.dc6635050bf5.ico", + client: "2xU5h-4bkWqA", + lastUsed: 1451519425, + }, + { + type: "tab", + title: "Firefox Nightly First Run Page", + url: "https://www.mozilla.org/en-US/firefox/nightly/firstrun/?oldversion=45.0a1", + icon: "moz-anno:favicon:https://www.mozilla.org/media/img/firefox/favicon-nightly.560395bbb2e1.png", + client: "2xU5h-4bkWqA", + lastUsed: 1451519420, + }, + { + // Should appear first for this client. + type: "tab", + title: "Mozilla Developer Network", + url: "https://developer.mozilla.org/en-US/", + icon: "moz-anno:favicon:https://developer.cdn.mozilla.net/static/img/favicon32.e02854fdcf73.png", + client: "2xU5h-4bkWqA", + lastUsed: 1451519725, + }, + ], + }, + { + id: "OL3EJCsdb2JD", + type: "client", + lastModified: 1492201200, + name: "desktop", + clientType: "desktop", + tabs: [], + }, +]; + +function setupSyncedTabsStubs({ + uiState = { status: UIState.STATUS_SIGNED_IN, syncEnabled: true }, + isConfiguredToSyncTabs = true, + hasSyncedThisSession = true, + tabClients = Cu.cloneInto(FIXTURE, {}), +} = {}) { + sinon.stub(UIState, "get").returns(uiState); + sinon.stub(SyncedTabs._internal, "getTabClients").resolves(tabClients); + sinon.stub(SyncedTabs._internal, "syncTabs").resolves(); + sinon + .stub(SyncedTabs._internal, "isConfiguredToSyncTabs") + .value(isConfiguredToSyncTabs); + sinon + .stub(SyncedTabs._internal, "hasSyncedThisSession") + .value(hasSyncedThisSession); +} + +async function testClean() { + sinon.restore(); + await new Promise(resolve => { + window.SidebarUI.browser.contentWindow.addEventListener( + "unload", + function () { + resolve(); + }, + { once: true } + ); + SidebarUI.hide(); + }); +} + +add_task(async function testSyncedTabsSidebarList() { + await SidebarUI.show("viewTabsSidebar"); + + Assert.equal( + SidebarUI.currentID, + "viewTabsSidebar", + "Sidebar should have SyncedTabs loaded" + ); + + let syncedTabsDeckComponent = + SidebarUI.browser.contentWindow.syncedTabsDeckComponent; + + Assert.ok(syncedTabsDeckComponent, "component exists"); + + setupSyncedTabsStubs(); + + await syncedTabsDeckComponent.updatePanel(); + // This is a hacky way of waiting for the view to render. The view renders + // after the following promise (a different instance of which is triggered + // in updatePanel) resolves, so we wait for it here as well + await syncedTabsDeckComponent.tabListComponent._store.getData(); + + Assert.ok(SyncedTabs._internal.getTabClients.called, "get clients called"); + + let selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + + Assert.ok( + selectedPanel.classList.contains("tabs-container"), + "tabs panel is selected" + ); + + Assert.equal( + selectedPanel.querySelectorAll(".tab").length, + 4, + "four tabs listed" + ); + Assert.equal( + selectedPanel.querySelectorAll(".client").length, + 3, + "three clients listed" + ); + Assert.equal( + selectedPanel.querySelectorAll(".client")[2].querySelectorAll(".empty") + .length, + 1, + "third client is empty" + ); + + // Verify that the tabs are sorted by last used time. + var expectedTabIndices = [[0], [2, 0, 1]]; + Array.prototype.forEach.call( + selectedPanel.querySelectorAll(".client"), + (clientNode, i) => { + checkItem(clientNode, FIXTURE[i]); + Array.prototype.forEach.call( + clientNode.querySelectorAll(".tab"), + (tabNode, j) => { + let tabIndex = expectedTabIndices[i][j]; + checkItem(tabNode, FIXTURE[i].tabs[tabIndex]); + } + ); + } + ); +}); + +add_task(testClean); + +add_task(async function testSyncedTabsSidebarFilteredList() { + await SidebarUI.show("viewTabsSidebar"); + let syncedTabsDeckComponent = + window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent; + + Assert.ok(syncedTabsDeckComponent, "component exists"); + + setupSyncedTabsStubs(); + + await syncedTabsDeckComponent.updatePanel(); + + let filterInput = + syncedTabsDeckComponent._window.document.querySelector(".tabsFilter"); + filterInput.value = "filter text"; + filterInput.blur(); + + // This is a hacky way of waiting for the view to render. The view renders + // after the following promise (a different instance of which is triggered + // in updatePanel) resolves, so we wait for it here as well + await syncedTabsDeckComponent.tabListComponent._store.getData("filter text"); + + let selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("tabs-container"), + "tabs panel is selected" + ); + + Assert.equal( + selectedPanel.querySelectorAll(".tab").length, + 4, + "four tabs listed" + ); + Assert.equal( + selectedPanel.querySelectorAll(".client").length, + 0, + "no clients are listed" + ); + + Assert.equal( + filterInput.value, + "filter text", + "filter text box has correct value" + ); + + // Tabs should not be sorted when filter is active. + let FIXTURE_TABS = FIXTURE.reduce( + (prev, client) => prev.concat(client.tabs), + [] + ); + + Array.prototype.forEach.call( + selectedPanel.querySelectorAll(".tab"), + (tabNode, i) => { + checkItem(tabNode, FIXTURE_TABS[i]); + } + ); + + // Removing the filter should resort tabs. + FIXTURE_TABS.sort((a, b) => b.lastUsed - a.lastUsed); + await syncedTabsDeckComponent.tabListComponent._store.getData(); + Array.prototype.forEach.call( + selectedPanel.querySelectorAll(".tab"), + (tabNode, i) => { + checkItem(tabNode, FIXTURE_TABS[i]); + } + ); +}); + +add_task(testClean); + +add_task(async function testSyncedTabsSidebarStatus() { + await SidebarUI.show("viewTabsSidebar"); + let syncedTabsDeckComponent = + window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent; + + Assert.ok(syncedTabsDeckComponent, "component exists"); + + setupSyncedTabsStubs({ + uiState: { status: UIState.STATUS_NOT_CONFIGURED }, + isConfiguredToSyncTabs: false, + hasSyncedThisSession: false, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + let selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("notAuthedInfo"), + "not-authed panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + uiState: { status: UIState.STATUS_NOT_VERIFIED }, + isConfiguredToSyncTabs: false, + hasSyncedThisSession: false, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("unverified"), + "unverified panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + uiState: { status: UIState.STATUS_SIGNED_IN, syncEnabled: false }, + isConfiguredToSyncTabs: false, + hasSyncedThisSession: false, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("syncDisabled"), + "sync disabled panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + uiState: { status: UIState.STATUS_LOGIN_FAILED }, + isConfiguredToSyncTabs: false, + hasSyncedThisSession: false, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("reauth"), + "reauth panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + isConfiguredToSyncTabs: false, + hasSyncedThisSession: false, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("tabs-disabled"), + "tabs disabled panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + isConfiguredToSyncTabs: true, + hasSyncedThisSession: false, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("tabs-fetching"), + "tabs fetch panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + tabClients: [], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("singleDeviceInfo"), + "tabs fetch panel is selected" + ); + sinon.restore(); + + setupSyncedTabsStubs({ + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + tabClients: [{ id: "mock" }], + }); + await syncedTabsDeckComponent.updatePanel(); + selectedPanel = syncedTabsDeckComponent.container.querySelector( + ".sync-state.selected" + ); + Assert.ok( + selectedPanel.classList.contains("tabs-container"), + "tabs panel is selected" + ); +}); + +add_task(testClean); + +add_task(async function testSyncedTabsSidebarContextMenu() { + await SidebarUI.show("viewTabsSidebar"); + let syncedTabsDeckComponent = + window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent; + + Assert.ok(syncedTabsDeckComponent, "component exists"); + + setupSyncedTabsStubs(); + + await syncedTabsDeckComponent.updatePanel(); + // This is a hacky way of waiting for the view to render. The view renders + // after the following promise (a different instance of which is triggered + // in updatePanel) resolves, so we wait for it here as well + await syncedTabsDeckComponent.tabListComponent._store.getData(); + + info("Right-clicking the search box should show text-related actions"); + let filterMenuItems = [ + "menuitem[cmd=cmd_undo]", + "menuseparator", + // We don't check whether the commands are enabled due to platform + // differences. On OS X and Windows, "cut" and "copy" are always enabled + // for HTML inputs; on Linux, they're only enabled if text is selected. + "menuitem[cmd=cmd_cut]", + "menuitem[cmd=cmd_copy]", + "menuitem[cmd=cmd_paste]", + "menuitem[cmd=cmd_delete]", + "menuseparator", + "menuitem[cmd=cmd_selectAll]", + "menuseparator", + "menuitem#syncedTabsRefreshFilter", + ]; + await testContextMenu( + syncedTabsDeckComponent, + "#SyncedTabsSidebarTabsFilterContext", + ".tabsFilter", + filterMenuItems + ); + + info("Right-clicking a tab should show additional actions"); + let tabMenuItems = [ + ["menuitem#syncedTabsOpenSelected", { hidden: false }], + ["menuitem#syncedTabsOpenSelectedInTab", { hidden: false }], + [ + "menu#syncedTabsOpenSelectedInContainerTab", + { + hidden: + !Services.prefs.getBoolPref("privacy.userContext.enabled", false) || + PrivateBrowsingUtils.isWindowPrivate(window), + }, + ], + ["menuitem#syncedTabsOpenSelectedInWindow", { hidden: false }], + ["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: false }], + ["menuseparator", { hidden: false }], + ["menuitem#syncedTabsBookmarkSelected", { hidden: false }], + ["menuitem#syncedTabsCopySelected", { hidden: false }], + ["menuseparator", { hidden: false }], + ["menuitem#syncedTabsOpenAllInTabs", { hidden: true }], + ["menuitem#syncedTabsManageDevices", { hidden: true }], + ["menuitem#syncedTabsRefresh", { hidden: false }], + ]; + await testContextMenu( + syncedTabsDeckComponent, + "#SyncedTabsSidebarContext", + "#tab-7cqCr77ptzX3-0", + tabMenuItems + ); + + info( + "Right-clicking a client should show the Open All in Tabs and Manage devices actions" + ); + let sidebarMenuItems = [ + ["menuitem#syncedTabsOpenSelected", { hidden: true }], + ["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }], + ["menu#syncedTabsOpenSelectedInContainerTab", { hidden: true }], + ["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }], + ["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }], + ["menuseparator", { hidden: true }], + ["menuitem#syncedTabsBookmarkSelected", { hidden: true }], + ["menuitem#syncedTabsCopySelected", { hidden: true }], + ["menuseparator", { hidden: true }], + ["menuitem#syncedTabsOpenAllInTabs", { hidden: false }], + ["menuitem#syncedTabsManageDevices", { hidden: false }], + ["menuitem#syncedTabsRefresh", { hidden: false }], + ]; + await testContextMenu( + syncedTabsDeckComponent, + "#SyncedTabsSidebarContext", + "#item-7cqCr77ptzX3", + sidebarMenuItems + ); + + info( + "Right-clicking a client without any tabs should not show the Open All in Tabs action" + ); + let menuItems = [ + ["menuitem#syncedTabsOpenSelected", { hidden: true }], + ["menuitem#syncedTabsOpenSelectedInTab", { hidden: true }], + ["menu#syncedTabsOpenSelectedInContainerTab", { hidden: true }], + ["menuitem#syncedTabsOpenSelectedInWindow", { hidden: true }], + ["menuitem#syncedTabsOpenSelectedInPrivateWindow", { hidden: true }], + ["menuseparator", { hidden: true }], + ["menuitem#syncedTabsBookmarkSelected", { hidden: true }], + ["menuitem#syncedTabsCopySelected", { hidden: true }], + ["menuseparator", { hidden: true }], + ["menuitem#syncedTabsOpenAllInTabs", { hidden: true }], + ["menuitem#syncedTabsManageDevices", { hidden: false }], + ["menuitem#syncedTabsRefresh", { hidden: false }], + ]; + await testContextMenu( + syncedTabsDeckComponent, + "#SyncedTabsSidebarContext", + "#item-OL3EJCsdb2JD", + menuItems + ); +}); + +add_task(testClean); + +function checkItem(node, item) { + Assert.ok(node.classList.contains("item"), "Node should have .item class"); + if (item.client) { + // tab items + Assert.equal( + node.querySelector(".item-title").textContent, + item.title, + "Node's title element's text should match item title" + ); + Assert.ok(node.classList.contains("tab"), "Node should have .tab class"); + Assert.equal( + node.dataset.url, + item.url, + "Node's URL should match item URL" + ); + Assert.equal( + node.getAttribute("title"), + item.title + "\n" + item.url, + "Tab node should have correct title attribute" + ); + } else { + // client items + Assert.equal( + node.querySelector(".item-title").textContent, + item.name, + "Node's title element's text should match client name" + ); + Assert.ok( + node.classList.contains("client"), + "Node should have .client class" + ); + Assert.equal(node.dataset.id, item.id, "Node's ID should match item ID"); + } +} + +async function testContextMenu( + syncedTabsDeckComponent, + contextSelector, + triggerSelector, + menuSelectors +) { + let contextMenu = document.querySelector(contextSelector); + let triggerElement = + syncedTabsDeckComponent._window.document.querySelector(triggerSelector); + let isClosed = triggerElement.classList.contains("closed"); + + let promisePopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + let chromeWindow = triggerElement.ownerGlobal.top; + let rect = triggerElement.getBoundingClientRect(); + let contentRect = chromeWindow.SidebarUI.browser.getBoundingClientRect(); + // The offsets in `rect` are relative to the content window, but + // `synthesizeMouseAtPoint` calls `nsIDOMWindowUtils.sendMouseEvent`, + // which interprets the offsets relative to the containing *chrome* window. + // This means we need to account for the width and height of any elements + // outside the `browser` element, like `#sidebar-header`. + let offsetX = contentRect.x + rect.x + rect.width / 2; + let offsetY = contentRect.y + rect.y + rect.height / 4; + + await EventUtils.synthesizeMouseAtPoint( + offsetX, + offsetY, + { + type: "contextmenu", + button: 2, + }, + chromeWindow + ); + await promisePopupShown; + is( + triggerElement.classList.contains("closed"), + isClosed, + "Showing the context menu shouldn't toggle the tab list" + ); + let menuitemClicked = await checkChildren(contextMenu, menuSelectors); + if (!menuitemClicked) { + let promisePopupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await promisePopupHidden; + } +} + +function checkChildren(node, selectors) { + is(node.children.length, selectors.length, "Menu item count doesn't match"); + let containerMenuShown; + for (let index = 0; index < node.children.length; index++) { + let child = node.children[index]; + let [selector, props] = [].concat(selectors[index]); + ok(selector, `Node at ${index} should have selector`); + ok(child.matches(selector), `Node ${index} should match ${selector}`); + if (props) { + Object.keys(props).forEach(prop => { + is(child[prop], props[prop], `${prop} value at ${index} should match`); + }); + } + if ( + selector === "menu#syncedTabsOpenSelectedInContainerTab" && + !props.hidden + ) { + containerMenuShown = child; + } + } + if (containerMenuShown) { + return testContainerMenu(containerMenuShown); + } + return false; +} + +async function testContainerMenu(menu) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + let menupopup = menu.getElementsByTagName("menupopup")[0]; + let menupopupShown = BrowserTestUtils.waitForEvent(menupopup, "popupshown"); + menu.openMenu(true); + await menupopupShown; + let shown = [1, 2, 3, 4]; + let hidden = [0]; + for (let id of shown) { + ok( + menupopup.querySelector(`menuitem[data-usercontextid="${id}"]`), + `User context id ${id} should exist` + ); + } + for (let id of hidden) { + ok( + !menupopup.querySelector(`menuitem[data-usercontextid="${id}"]`), + `User context id ${id} shouldn't exist` + ); + } + const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + menupopup.activateItem( + menupopup.querySelector( + `menuitem[data-usercontextid="${shown[shown.length - 1]}"]` + ) + ); + let newTab = await newTabPromise; + ok( + newTab.hasAttribute("usercontextid"), + `Tab with usercontextid = ${shown[shown.length - 1]} should be opened` + ); + registerCleanupFunction(() => BrowserTestUtils.removeTab(newTab)); + return true; +} -- cgit v1.2.3