diff options
Diffstat (limited to '')
7 files changed, 869 insertions, 0 deletions
diff --git a/browser/components/syncedtabs/test/xpcshell/head.js b/browser/components/syncedtabs/test/xpcshell/head.js new file mode 100644 index 0000000000..c390b71011 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/head.js @@ -0,0 +1,12 @@ +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +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..07f2a1b296 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js @@ -0,0 +1,36 @@ +"use strict"; + +let { EventEmitter } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/EventEmitter.sys.mjs" +); + +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..9162325081 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js @@ -0,0 +1,263 @@ +"use strict"; + +let { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); +let { SyncedTabsDeckComponent } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/SyncedTabsDeckComponent.sys.mjs" +); +let { SyncedTabsListStore } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/SyncedTabsListStore.sys.mjs" +); +let { SyncedTabsDeckStore } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/SyncedTabsDeckStore.sys.mjs" +); +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +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..a75ded8c8a --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js @@ -0,0 +1,69 @@ +"use strict"; + +let { SyncedTabsDeckStore } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/SyncedTabsDeckStore.sys.mjs" +); + +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..36138aace3 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js @@ -0,0 +1,289 @@ +"use strict"; + +let { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); +let { SyncedTabsListStore } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/SyncedTabsListStore.sys.mjs" +); + +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..734baac254 --- /dev/null +++ b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js @@ -0,0 +1,190 @@ +"use strict"; + +let { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); +let { TabListComponent } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/TabListComponent.sys.mjs" +); +let { SyncedTabsListStore } = ChromeUtils.importESModule( + "resource:///modules/syncedtabs/SyncedTabsListStore.sys.mjs" +); + +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] |