From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- .../components/syncedtabs/test/browser/browser.ini | 4 + .../test/browser/browser_sidebar_syncedtabslist.js | 652 +++++++++++++++++++++ browser/components/syncedtabs/test/browser/head.js | 4 + .../components/syncedtabs/test/xpcshell/head.js | 10 + .../syncedtabs/test/xpcshell/test_EventEmitter.js | 36 ++ .../test/xpcshell/test_SyncedTabsDeckComponent.js | 261 +++++++++ .../test/xpcshell/test_SyncedTabsDeckStore.js | 69 +++ .../test/xpcshell/test_SyncedTabsListStore.js | 293 +++++++++ .../test/xpcshell/test_TabListComponent.js | 190 ++++++ .../syncedtabs/test/xpcshell/xpcshell.ini | 10 + 10 files changed, 1529 insertions(+) create mode 100644 browser/components/syncedtabs/test/browser/browser.ini create mode 100644 browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js create mode 100644 browser/components/syncedtabs/test/browser/head.js create mode 100644 browser/components/syncedtabs/test/xpcshell/head.js create mode 100644 browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js create mode 100644 browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js create mode 100644 browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js create mode 100644 browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js create mode 100644 browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js create mode 100644 browser/components/syncedtabs/test/xpcshell/xpcshell.ini (limited to 'browser/components/syncedtabs/test') diff --git a/browser/components/syncedtabs/test/browser/browser.ini b/browser/components/syncedtabs/test/browser/browser.ini new file mode 100644 index 0000000000..02fa364f10 --- /dev/null +++ b/browser/components/syncedtabs/test/browser/browser.ini @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = head.js + +[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..9cd4c886cf --- /dev/null +++ b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js @@ -0,0 +1,652 @@ +"use strict"; + +const { SyncedTabs } = ChromeUtils.import( + "resource://services-sync/SyncedTabs.jsm" +); +const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm"); + +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; +} diff --git a/browser/components/syncedtabs/test/browser/head.js b/browser/components/syncedtabs/test/browser/head.js new file mode 100644 index 0000000000..800bb0aa91 --- /dev/null +++ b/browser/components/syncedtabs/test/browser/head.js @@ -0,0 +1,4 @@ +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); diff --git a/browser/components/syncedtabs/test/xpcshell/head.js b/browser/components/syncedtabs/test/xpcshell/head.js new file mode 100644 index 0000000000..609c005652 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/head.js @@ -0,0 +1,10 @@ +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() { + return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js"); +}); + +do_get_profile(); // fxa needs a profile directory for storage. diff --git a/browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js b/browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js new file mode 100644 index 0000000000..4836dc39da --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js @@ -0,0 +1,36 @@ +"use strict"; + +let { EventEmitter } = ChromeUtils.import( + "resource:///modules/syncedtabs/EventEmitter.jsm" +); + +add_task(async function testSingleListener() { + let eventEmitter = new EventEmitter(); + let spy = sinon.spy(); + + eventEmitter.on("click", spy); + eventEmitter.emit("click", "foo", "bar"); + Assert.ok(spy.calledOnce); + Assert.ok(spy.calledWith("foo", "bar")); + + eventEmitter.off("click", spy); + eventEmitter.emit("click"); + Assert.ok(spy.calledOnce); +}); + +add_task(async function testMultipleListeners() { + let eventEmitter = new EventEmitter(); + let spy1 = sinon.spy(); + let spy2 = sinon.spy(); + + eventEmitter.on("some_event", spy1); + eventEmitter.on("some_event", spy2); + eventEmitter.emit("some_event"); + Assert.ok(spy1.calledOnce); + Assert.ok(spy2.calledOnce); + + eventEmitter.off("some_event", spy1); + eventEmitter.emit("some_event"); + Assert.ok(spy1.calledOnce); + Assert.ok(spy2.calledTwice); +}); diff --git a/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js new file mode 100644 index 0000000000..baa7e64b7e --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js @@ -0,0 +1,261 @@ +"use strict"; + +let { SyncedTabs } = ChromeUtils.import( + "resource://services-sync/SyncedTabs.jsm" +); +let { SyncedTabsDeckComponent } = ChromeUtils.import( + "resource:///modules/syncedtabs/SyncedTabsDeckComponent.js" +); +let { SyncedTabsListStore } = ChromeUtils.import( + "resource:///modules/syncedtabs/SyncedTabsListStore.js" +); +let { SyncedTabsDeckStore } = ChromeUtils.import( + "resource:///modules/syncedtabs/SyncedTabsDeckStore.js" +); +const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm"); + +add_task(async function testInitUninit() { + let deckStore = new SyncedTabsDeckStore(); + let listComponent = {}; + let mockWindow = {}; + + let ViewMock = sinon.stub(); + let view = { render: sinon.spy(), destroy: sinon.spy(), container: {} }; + ViewMock.returns(view); + + sinon.stub(SyncedTabs, "syncTabs").callsFake(() => Promise.resolve()); + + sinon.spy(deckStore, "on"); + sinon.stub(deckStore, "setPanels"); + + let component = new SyncedTabsDeckComponent({ + window: mockWindow, + deckStore, + listComponent, + SyncedTabs, + DeckView: ViewMock, + }); + + sinon.stub(component, "updatePanel"); + + component.init(); + + Assert.ok(SyncedTabs.syncTabs.called); + SyncedTabs.syncTabs.restore(); + + Assert.ok(ViewMock.calledWithNew(), "view is instantiated"); + Assert.equal(ViewMock.args[0][0], mockWindow); + Assert.equal(ViewMock.args[0][1], listComponent); + Assert.ok( + ViewMock.args[0][2].onConnectDeviceClick, + "view is passed onConnectDeviceClick prop" + ); + Assert.ok( + ViewMock.args[0][2].onSyncPrefClick, + "view is passed onSyncPrefClick prop" + ); + + Assert.equal( + component.container, + view.container, + "component returns view's container" + ); + + Assert.ok(deckStore.on.calledOnce, "listener is added to store"); + Assert.equal(deckStore.on.args[0][0], "change"); + // Object.values only in nightly + let values = Object.keys(component.PANELS).map(k => component.PANELS[k]); + Assert.ok( + deckStore.setPanels.calledWith(values), + "panels are set on deck store" + ); + + Assert.ok(component.updatePanel.called); + + deckStore.emit("change", "mock state"); + Assert.ok( + view.render.calledWith("mock state"), + "view.render is called on state change" + ); + + component.uninit(); + + Assert.ok(view.destroy.calledOnce, "view is destroyed on uninit"); +}); + +add_task(async function testObserver() { + let deckStore = new SyncedTabsDeckStore(); + let listStore = new SyncedTabsListStore(SyncedTabs); + let listComponent = {}; + let mockWindow = {}; + + let ViewMock = sinon.stub(); + let view = { render: sinon.spy(), destroy: sinon.spy(), container: {} }; + ViewMock.returns(view); + + sinon.stub(SyncedTabs, "syncTabs").callsFake(() => Promise.resolve()); + + sinon.spy(deckStore, "on"); + sinon.stub(deckStore, "setPanels"); + + sinon.stub(listStore, "getData"); + + let component = new SyncedTabsDeckComponent({ + window: mockWindow, + deckStore, + listStore, + listComponent, + SyncedTabs, + DeckView: ViewMock, + }); + + sinon.spy(component, "observe"); + sinon.stub(component, "updatePanel"); + sinon.stub(component, "updateDir"); + + component.init(); + SyncedTabs.syncTabs.restore(); + Assert.ok(component.updatePanel.called, "triggers panel update during init"); + Assert.ok( + component.updateDir.called, + "triggers UI direction update during init" + ); + + Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED); + + Assert.ok( + component.observe.calledWith(null, SyncedTabs.TOPIC_TABS_CHANGED), + "component is notified" + ); + + Assert.ok(listStore.getData.called, "gets list data"); + Assert.ok(component.updatePanel.calledTwice, "triggers panel update"); + + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + + Assert.ok( + component.observe.calledWith(null, UIState.ON_UPDATE), + "component is notified of FxA/Sync UI Update" + ); + Assert.equal( + component.updatePanel.callCount, + 3, + "triggers panel update again" + ); + + Services.locale.availableLocales = ["ab-CD"]; + Services.locale.requestedLocales = ["ab-CD"]; + + Assert.ok( + component.updateDir.calledTwice, + "locale change triggers UI direction update" + ); + + Services.prefs.setStringPref("intl.l10n.pseudo", "bidi"); + + Assert.equal( + component.updateDir.callCount, + 3, + "pref change triggers UI direction update" + ); +}); + +add_task(async function testPanelStatus() { + let deckStore = new SyncedTabsDeckStore(); + let listStore = new SyncedTabsListStore(); + let listComponent = {}; + let SyncedTabsMock = { + getTabClients() {}, + }; + + sinon.stub(listStore, "getData"); + + let component = new SyncedTabsDeckComponent({ + deckStore, + listComponent, + SyncedTabs: SyncedTabsMock, + }); + + sinon.stub(UIState, "get").returns({ status: UIState.STATUS_NOT_CONFIGURED }); + let result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.NOT_AUTHED_INFO); + UIState.get.restore(); + + sinon.stub(UIState, "get").returns({ status: UIState.STATUS_NOT_VERIFIED }); + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.UNVERIFIED); + UIState.get.restore(); + + sinon.stub(UIState, "get").returns({ status: UIState.STATUS_LOGIN_FAILED }); + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.LOGIN_FAILED); + UIState.get.restore(); + + sinon + .stub(UIState, "get") + .returns({ status: UIState.STATUS_SIGNED_IN, syncEnabled: false }); + SyncedTabsMock.isConfiguredToSyncTabs = true; + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.SYNC_DISABLED); + UIState.get.restore(); + + sinon + .stub(UIState, "get") + .returns({ status: UIState.STATUS_SIGNED_IN, syncEnabled: true }); + SyncedTabsMock.isConfiguredToSyncTabs = false; + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.TABS_DISABLED); + + SyncedTabsMock.isConfiguredToSyncTabs = true; + + SyncedTabsMock.hasSyncedThisSession = false; + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.TABS_FETCHING); + + SyncedTabsMock.hasSyncedThisSession = true; + + let clients = []; + sinon + .stub(SyncedTabsMock, "getTabClients") + .callsFake(() => Promise.resolve(clients)); + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.SINGLE_DEVICE_INFO); + + clients = ["mock-client"]; + result = await component.getPanelStatus(); + Assert.equal(result, component.PANELS.TABS_CONTAINER); + + sinon + .stub(component, "getPanelStatus") + .callsFake(() => Promise.resolve("mock-panelId")); + sinon.spy(deckStore, "selectPanel"); + await component.updatePanel(); + Assert.ok(deckStore.selectPanel.calledWith("mock-panelId")); +}); + +add_task(async function testActions() { + let windowMock = {}; + let chromeWindowMock = { + gSync: { + openPrefs() {}, + openConnectAnotherDevice() {}, + }, + }; + sinon.spy(chromeWindowMock.gSync, "openPrefs"); + sinon.spy(chromeWindowMock.gSync, "openConnectAnotherDevice"); + + let getChromeWindowMock = sinon.stub(); + getChromeWindowMock.returns(chromeWindowMock); + + let component = new SyncedTabsDeckComponent({ + window: windowMock, + getChromeWindowMock, + }); + + component.openConnectDevice(); + Assert.ok(chromeWindowMock.gSync.openConnectAnotherDevice.called); + + component.openSyncPrefs(); + Assert.ok(getChromeWindowMock.calledWith(windowMock)); + Assert.ok(chromeWindowMock.gSync.openPrefs.called); +}); diff --git a/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js new file mode 100644 index 0000000000..f909fc0faa --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js @@ -0,0 +1,69 @@ +"use strict"; + +let { SyncedTabsDeckStore } = ChromeUtils.import( + "resource:///modules/syncedtabs/SyncedTabsDeckStore.js" +); + +add_task(async function testSelectUnkownPanel() { + let deckStore = new SyncedTabsDeckStore(); + let spy = sinon.spy(); + + deckStore.on("change", spy); + deckStore.selectPanel("foo"); + + Assert.ok(!spy.called); +}); + +add_task(async function testSetPanels() { + let deckStore = new SyncedTabsDeckStore(); + let spy = sinon.spy(); + + deckStore.on("change", spy); + deckStore.setPanels(["panel1", "panel2"]); + + Assert.ok( + spy.calledWith({ + panels: [ + { id: "panel1", selected: false }, + { id: "panel2", selected: false }, + ], + isUpdatable: false, + }) + ); +}); + +add_task(async function testSelectPanel() { + let deckStore = new SyncedTabsDeckStore(); + let spy = sinon.spy(); + + deckStore.setPanels(["panel1", "panel2"]); + + deckStore.on("change", spy); + deckStore.selectPanel("panel2"); + + Assert.ok( + spy.calledWith({ + panels: [ + { id: "panel1", selected: false }, + { id: "panel2", selected: true }, + ], + isUpdatable: true, + }) + ); + + deckStore.selectPanel("panel2"); + Assert.ok(spy.calledOnce, "doesn't trigger unless panel changes"); +}); + +add_task(async function testSetPanelsSameArray() { + let deckStore = new SyncedTabsDeckStore(); + let spy = sinon.spy(); + deckStore.on("change", spy); + + let panels = ["panel1", "panel2"]; + + deckStore.setPanels(panels); + deckStore.setPanels(panels); + + Assert.ok(spy.calledOnce, "doesn't trigger unless set of panels changes"); +}); diff --git a/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js new file mode 100644 index 0000000000..4f68bcad29 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js @@ -0,0 +1,293 @@ +"use strict"; + +let { SyncedTabs } = ChromeUtils.import( + "resource://services-sync/SyncedTabs.jsm" +); +let { SyncedTabsListStore } = ChromeUtils.import( + "resource:///modules/syncedtabs/SyncedTabsListStore.js" +); + +const FIXTURE = [ + { + id: "2xU5h-4bkWqA", + type: "client", + lastModified: 1492201200, + name: "laptop", + isMobile: false, + 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, + }, + ], + }, + { + id: "OL3EJCsdb2JD", + type: "client", + lastModified: 1492201200, + name: "desktop", + isMobile: false, + tabs: [], + }, +]; + +add_task(async function testGetDataEmpty() { + let store = new SyncedTabsListStore(SyncedTabs); + let spy = sinon.spy(); + + sinon.stub(SyncedTabs, "getTabClients").callsFake(() => { + return Promise.resolve([]); + }); + store.on("change", spy); + + await store.getData(); + + Assert.ok(SyncedTabs.getTabClients.calledWith("")); + Assert.ok( + spy.calledWith({ + clients: [], + canUpdateAll: false, + canUpdateInput: false, + filter: "", + inputFocused: false, + }) + ); + + await store.getData("filter"); + + Assert.ok(SyncedTabs.getTabClients.calledWith("filter")); + Assert.ok( + spy.calledWith({ + clients: [], + canUpdateAll: false, + canUpdateInput: true, + filter: "filter", + inputFocused: false, + }) + ); + + SyncedTabs.getTabClients.restore(); +}); + +add_task(async function testRowSelectionWithoutFilter() { + let store = new SyncedTabsListStore(SyncedTabs); + let spy = sinon.spy(); + + sinon.stub(SyncedTabs, "getTabClients").callsFake(() => { + return Promise.resolve(FIXTURE); + }); + + await store.getData(); + SyncedTabs.getTabClients.restore(); + + store.on("change", spy); + + store.selectRow(0, -1); + Assert.ok(spy.args[0][0].canUpdateAll, "can update the whole view"); + Assert.ok(spy.args[0][0].clients[0].selected, "first client is selected"); + + store.moveSelectionUp(); + Assert.ok( + spy.calledOnce, + "can't move up past first client, no change triggered" + ); + + store.selectRow(0, 0); + Assert.ok( + spy.args[1][0].clients[0].tabs[0].selected, + "first tab of first client is selected" + ); + + store.selectRow(0, 0); + Assert.ok(spy.calledTwice, "selecting same row doesn't trigger change"); + + store.selectRow(0, 1); + Assert.ok( + spy.args[2][0].clients[0].tabs[1].selected, + "second tab of first client is selected" + ); + + store.selectRow(1); + Assert.ok(spy.args[3][0].clients[1].selected, "second client is selected"); + + store.moveSelectionDown(); + Assert.equal( + spy.callCount, + 4, + "can't move selection down past last client, no change triggered" + ); + + store.moveSelectionUp(); + Assert.equal(spy.callCount, 5, "changed"); + Assert.ok( + spy.args[4][0].clients[0].tabs[FIXTURE[0].tabs.length - 1].selected, + "move selection up from client selects last tab of previous client" + ); + + store.moveSelectionUp(); + Assert.ok( + spy.args[5][0].clients[0].tabs[FIXTURE[0].tabs.length - 2].selected, + "move selection up from tab selects previous tab of client" + ); +}); + +add_task(async function testToggleBranches() { + let store = new SyncedTabsListStore(SyncedTabs); + let spy = sinon.spy(); + + sinon.stub(SyncedTabs, "getTabClients").callsFake(() => { + return Promise.resolve(FIXTURE); + }); + + await store.getData(); + SyncedTabs.getTabClients.restore(); + + store.selectRow(0); + store.on("change", spy); + + let clientId = FIXTURE[0].id; + store.closeBranch(clientId); + Assert.ok(spy.args[0][0].clients[0].closed, "first client is closed"); + + store.openBranch(clientId); + Assert.ok(!spy.args[1][0].clients[0].closed, "first client is open"); + + store.toggleBranch(clientId); + Assert.ok(spy.args[2][0].clients[0].closed, "first client is toggled closed"); + + store.moveSelectionDown(); + Assert.ok( + spy.args[3][0].clients[1].selected, + "selection skips tabs if client is closed" + ); + + store.moveSelectionUp(); + Assert.ok( + spy.args[4][0].clients[0].selected, + "selection skips tabs if client is closed" + ); +}); + +add_task(async function testRowSelectionWithFilter() { + let store = new SyncedTabsListStore(SyncedTabs); + let spy = sinon.spy(); + + sinon.stub(SyncedTabs, "getTabClients").callsFake(() => { + return Promise.resolve(FIXTURE); + }); + + await store.getData("filter"); + SyncedTabs.getTabClients.restore(); + + store.on("change", spy); + + store.selectRow(0); + Assert.ok( + spy.args[0][0].clients[0].tabs[0].selected, + "first tab is selected" + ); + + store.moveSelectionUp(); + Assert.ok( + spy.calledOnce, + "can't move up past first tab, no change triggered" + ); + + store.moveSelectionDown(); + Assert.ok( + spy.args[1][0].clients[0].tabs[1].selected, + "selection skips tabs if client is closed" + ); + + store.moveSelectionDown(); + Assert.equal( + spy.callCount, + 2, + "can't move selection down past last tab, no change triggered" + ); + + store.selectRow(1); + Assert.equal(spy.callCount, 2, "doesn't trigger change if same row selected"); +}); + +add_task(async function testFilterAndClearFilter() { + let store = new SyncedTabsListStore(SyncedTabs); + let spy = sinon.spy(); + + sinon.stub(SyncedTabs, "getTabClients").callsFake(() => { + return Promise.resolve(FIXTURE); + }); + store.on("change", spy); + + await store.getData("filter"); + + Assert.ok(SyncedTabs.getTabClients.calledWith("filter")); + Assert.ok(!spy.args[0][0].canUpdateAll, "can't update all"); + Assert.ok(spy.args[0][0].canUpdateInput, "can update just input"); + + store.selectRow(0); + + Assert.equal(spy.args[1][0].filter, "filter"); + Assert.ok(spy.args[1][0].clients[0].tabs[0].selected, "tab is selected"); + + await store.clearFilter(); + + Assert.ok(SyncedTabs.getTabClients.calledWith("")); + Assert.ok(!spy.args[2][0].canUpdateAll, "can't update all"); + Assert.ok(!spy.args[2][0].canUpdateInput, "can't just update input"); + + Assert.equal(spy.args[2][0].filter, ""); + Assert.ok( + !spy.args[2][0].clients[0].tabs[0].selected, + "tab is no longer selected" + ); + + SyncedTabs.getTabClients.restore(); +}); + +add_task(async function testFocusBlurInput() { + let store = new SyncedTabsListStore(SyncedTabs); + let spy = sinon.spy(); + + sinon.stub(SyncedTabs, "getTabClients").callsFake(() => { + return Promise.resolve(FIXTURE); + }); + store.on("change", spy); + + await store.getData(); + SyncedTabs.getTabClients.restore(); + + Assert.ok(!spy.args[0][0].canUpdateAll, "must rerender all"); + + store.selectRow(0); + Assert.ok(!spy.args[1][0].inputFocused, "input is not focused"); + Assert.ok(spy.args[1][0].clients[0].selected, "client is selected"); + Assert.ok(spy.args[1][0].clients[0].focused, "client is focused"); + + store.focusInput(); + Assert.ok(spy.args[2][0].inputFocused, "input is focused"); + Assert.ok(spy.args[2][0].clients[0].selected, "client is still selected"); + Assert.ok(!spy.args[2][0].clients[0].focused, "client is no longer focused"); + + store.blurInput(); + Assert.ok(!spy.args[3][0].inputFocused, "input is not focused"); + Assert.ok(spy.args[3][0].clients[0].selected, "client is selected"); + Assert.ok(spy.args[3][0].clients[0].focused, "client is focused"); +}); diff --git a/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js new file mode 100644 index 0000000000..7ab4b41128 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js @@ -0,0 +1,190 @@ +"use strict"; + +let { SyncedTabs } = ChromeUtils.import( + "resource://services-sync/SyncedTabs.jsm" +); +let { TabListComponent } = ChromeUtils.import( + "resource:///modules/syncedtabs/TabListComponent.js" +); +let { SyncedTabsListStore } = ChromeUtils.import( + "resource:///modules/syncedtabs/SyncedTabsListStore.js" +); + +const ACTION_METHODS = [ + "onSelectRow", + "onOpenTab", + "onOpenTabs", + "onMoveSelectionDown", + "onMoveSelectionUp", + "onToggleBranch", + "onBookmarkTab", + "onSyncRefresh", + "onFilter", + "onClearFilter", + "onFilterFocus", + "onFilterBlur", +]; + +add_task(async function testInitUninit() { + let store = new SyncedTabsListStore(); + let ViewMock = sinon.stub(); + let view = { render() {}, destroy() {} }; + let mockWindow = {}; + + ViewMock.returns(view); + + sinon.spy(view, "render"); + sinon.spy(view, "destroy"); + + sinon.spy(store, "on"); + sinon.stub(store, "getData"); + sinon.stub(store, "focusInput"); + + let component = new TabListComponent({ + window: mockWindow, + store, + View: ViewMock, + SyncedTabs, + }); + + for (let action of ACTION_METHODS) { + sinon.stub(component, action); + } + + component.init(); + + Assert.ok(ViewMock.calledWithNew(), "view is instantiated"); + Assert.ok(store.on.calledOnce, "listener is added to store"); + Assert.equal(store.on.args[0][0], "change"); + Assert.ok( + view.render.calledWith({ clients: [] }), + "render is called on view instance" + ); + Assert.ok(store.getData.calledOnce, "store gets initial data"); + Assert.ok(store.focusInput.calledOnce, "input field is focused"); + + for (let method of ACTION_METHODS) { + let action = ViewMock.args[0][1][method]; + Assert.ok(action, method + " action is passed to View"); + action("foo", "bar"); + Assert.ok( + component[method].calledWith("foo", "bar"), + method + " action passed to View triggers the component method with args" + ); + } + + store.emit("change", "mock state"); + Assert.ok( + view.render.secondCall.calledWith("mock state"), + "view.render is called on state change" + ); + + component.uninit(); + Assert.ok(view.destroy.calledOnce, "view is destroyed on uninit"); +}); + +add_task(async function testActions() { + let store = new SyncedTabsListStore(); + let chromeWindowMock = { + gBrowser: { + loadTabs() {}, + }, + }; + let getChromeWindowMock = sinon.stub(); + getChromeWindowMock.returns(chromeWindowMock); + let clipboardHelperMock = { + copyString() {}, + }; + let windowMock = { + top: { + PlacesCommandHook: { + bookmarkLink() { + return Promise.resolve(); + }, + }, + }, + openDialog() {}, + openTrustedLinkIn() {}, + }; + let component = new TabListComponent({ + window: windowMock, + store, + View: null, + SyncedTabs, + clipboardHelper: clipboardHelperMock, + getChromeWindow: getChromeWindowMock, + }); + + sinon.stub(store, "getData"); + component.onFilter("query"); + Assert.ok(store.getData.calledWith("query")); + + sinon.stub(store, "clearFilter"); + component.onClearFilter(); + Assert.ok(store.clearFilter.called); + + sinon.stub(store, "focusInput"); + component.onFilterFocus(); + Assert.ok(store.focusInput.called); + + sinon.stub(store, "blurInput"); + component.onFilterBlur(); + Assert.ok(store.blurInput.called); + + sinon.stub(store, "selectRow"); + component.onSelectRow([-1, -1]); + Assert.ok(store.selectRow.calledWith(-1, -1)); + + sinon.stub(store, "moveSelectionDown"); + component.onMoveSelectionDown(); + Assert.ok(store.moveSelectionDown.called); + + sinon.stub(store, "moveSelectionUp"); + component.onMoveSelectionUp(); + Assert.ok(store.moveSelectionUp.called); + + sinon.stub(store, "toggleBranch"); + component.onToggleBranch("foo-id"); + Assert.ok(store.toggleBranch.calledWith("foo-id")); + + sinon.spy(windowMock.top.PlacesCommandHook, "bookmarkLink"); + component.onBookmarkTab("uri", "title"); + Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][0], "uri"); + Assert.equal( + windowMock.top.PlacesCommandHook.bookmarkLink.args[0][1], + "title" + ); + + sinon.spy(windowMock, "openTrustedLinkIn"); + component.onOpenTab("uri", "where", "params"); + Assert.ok(windowMock.openTrustedLinkIn.calledWith("uri", "where", "params")); + + sinon.spy(chromeWindowMock.gBrowser, "loadTabs"); + let tabsToOpen = ["uri1", "uri2"]; + component.onOpenTabs(tabsToOpen, "where"); + Assert.ok(getChromeWindowMock.calledWith(windowMock)); + Assert.ok( + chromeWindowMock.gBrowser.loadTabs.calledWith(tabsToOpen, { + inBackground: false, + replace: false, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }) + ); + component.onOpenTabs(tabsToOpen, "tabshifted"); + Assert.ok( + chromeWindowMock.gBrowser.loadTabs.calledWith(tabsToOpen, { + inBackground: true, + replace: false, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }) + ); + + sinon.spy(clipboardHelperMock, "copyString"); + component.onCopyTabLocation("uri"); + Assert.ok(clipboardHelperMock.copyString.calledWith("uri")); + + sinon.stub(SyncedTabs, "syncTabs"); + component.onSyncRefresh(); + Assert.ok(SyncedTabs.syncTabs.calledWith(true)); + SyncedTabs.syncTabs.restore(); +}); diff --git a/browser/components/syncedtabs/test/xpcshell/xpcshell.ini b/browser/components/syncedtabs/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..5b18e0757e --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +head = head.js +firefox-appdir = browser + +[test_EventEmitter.js] +[test_SyncedTabsDeckStore.js] +[test_SyncedTabsListStore.js] +[test_SyncedTabsDeckComponent.js] +[test_TabListComponent.js] -- cgit v1.2.3