summaryrefslogtreecommitdiffstats
path: root/browser/components/syncedtabs/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/syncedtabs/test/xpcshell')
-rw-r--r--browser/components/syncedtabs/test/xpcshell/.eslintrc.js7
-rw-r--r--browser/components/syncedtabs/test/xpcshell/head.js29
-rw-r--r--browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js35
-rw-r--r--browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js218
-rw-r--r--browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js64
-rw-r--r--browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js266
-rw-r--r--browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js155
-rw-r--r--browser/components/syncedtabs/test/xpcshell/xpcshell.ini10
8 files changed, 784 insertions, 0 deletions
diff --git a/browser/components/syncedtabs/test/xpcshell/.eslintrc.js b/browser/components/syncedtabs/test/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/browser/components/syncedtabs/test/xpcshell/head.js b/browser/components/syncedtabs/test/xpcshell/head.js
new file mode 100644
index 000000000..00055231c
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/head.js
@@ -0,0 +1,29 @@
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
+ return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+do_get_profile(); // fxa needs a profile directory for storage.
+
+// Create a window polyfill so sinon can load
+let window = {
+ document: {},
+ location: {},
+ setTimeout: setTimeout,
+ setInterval: setInterval,
+ clearTimeout: clearTimeout,
+ clearinterval: clearInterval
+};
+let self = window;
+
+// Load mocking/stubbing library, sinon
+// docs: http://sinonjs.org/docs/
+/* global sinon */
+let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
+loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
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 000000000..bc73ac621
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/test_EventEmitter.js
@@ -0,0 +1,35 @@
+"use strict";
+
+let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {});
+
+add_task(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(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 000000000..3d748b33c
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
@@ -0,0 +1,218 @@
+"use strict";
+
+let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
+let { SyncedTabsDeckComponent } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckComponent.js", {});
+let { TabListComponent } = Cu.import("resource:///modules/syncedtabs/TabListComponent.js", {});
+let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
+let { SyncedTabsDeckStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckStore.js", {});
+let { TabListView } = Cu.import("resource:///modules/syncedtabs/TabListView.js", {});
+let { DeckView } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckView.js", {});
+
+
+add_task(function* testInitUninit() {
+ let deckStore = new SyncedTabsDeckStore();
+ let listComponent = {};
+
+ let ViewMock = sinon.stub();
+ let view = {render: sinon.spy(), destroy: sinon.spy(), container: {}};
+ ViewMock.returns(view);
+
+ sinon.stub(SyncedTabs, "syncTabs", () => Promise.resolve());
+
+ sinon.spy(deckStore, "on");
+ sinon.stub(deckStore, "setPanels");
+
+ let component = new SyncedTabsDeckComponent({
+ window,
+ 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], window);
+ Assert.equal(ViewMock.args[0][1], listComponent);
+ Assert.ok(ViewMock.args[0][2].onAndroidClick,
+ "view is passed onAndroidClick prop");
+ Assert.ok(ViewMock.args[0][2].oniOSClick,
+ "view is passed oniOSClick 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");
+});
+
+
+function waitForObserver() {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver((subject, topic) => {
+ resolve();
+ }, SyncedTabs.TOPIC_TABS_CHANGED, false);
+ });
+}
+
+add_task(function* testObserver() {
+ let deckStore = new SyncedTabsDeckStore();
+ let listStore = new SyncedTabsListStore(SyncedTabs);
+ let listComponent = {};
+
+ let ViewMock = sinon.stub();
+ let view = {render: sinon.spy(), destroy: sinon.spy(), container: {}};
+ ViewMock.returns(view);
+
+ sinon.stub(SyncedTabs, "syncTabs", () => Promise.resolve());
+
+ sinon.spy(deckStore, "on");
+ sinon.stub(deckStore, "setPanels");
+
+ sinon.stub(listStore, "getData");
+
+ let component = new SyncedTabsDeckComponent({
+ window,
+ deckStore,
+ listStore,
+ listComponent,
+ SyncedTabs,
+ DeckView: ViewMock,
+ });
+
+ sinon.spy(component, "observe");
+ sinon.stub(component, "updatePanel");
+
+ component.init();
+ SyncedTabs.syncTabs.restore();
+ Assert.ok(component.updatePanel.called, "triggers panel 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, FxAccountsCommon.ONLOGIN_NOTIFICATION, "");
+
+ Assert.ok(component.observe.calledWith(null, FxAccountsCommon.ONLOGIN_NOTIFICATION, ""),
+ "component is notified of login");
+ Assert.equal(component.updatePanel.callCount, 3, "triggers panel update again");
+});
+
+add_task(function* testPanelStatus() {
+ let deckStore = new SyncedTabsDeckStore();
+ let listStore = new SyncedTabsListStore();
+ let listComponent = {};
+ let fxAccounts = {
+ accountStatus() {}
+ };
+ let SyncedTabsMock = {
+ getTabClients() {}
+ };
+
+ sinon.stub(listStore, "getData");
+
+
+ let component = new SyncedTabsDeckComponent({
+ fxAccounts,
+ deckStore,
+ listComponent,
+ SyncedTabs: SyncedTabsMock,
+ });
+
+ let isAuthed = false;
+ sinon.stub(fxAccounts, "accountStatus", () => Promise.resolve(isAuthed));
+ let result = yield component.getPanelStatus();
+ Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
+
+ isAuthed = true;
+
+ SyncedTabsMock.isConfiguredToSyncTabs = false;
+ result = yield component.getPanelStatus();
+ Assert.equal(result, component.PANELS.TABS_DISABLED);
+
+ SyncedTabsMock.isConfiguredToSyncTabs = true;
+
+ SyncedTabsMock.hasSyncedThisSession = false;
+ result = yield component.getPanelStatus();
+ Assert.equal(result, component.PANELS.TABS_FETCHING);
+
+ SyncedTabsMock.hasSyncedThisSession = true;
+
+ let clients = [];
+ sinon.stub(SyncedTabsMock, "getTabClients", () => Promise.resolve(clients));
+ result = yield component.getPanelStatus();
+ Assert.equal(result, component.PANELS.SINGLE_DEVICE_INFO);
+
+ clients = ["mock-client"];
+ result = yield component.getPanelStatus();
+ Assert.equal(result, component.PANELS.TABS_CONTAINER);
+
+ fxAccounts.accountStatus.restore();
+ sinon.stub(fxAccounts, "accountStatus", () => Promise.reject("err"));
+ result = yield component.getPanelStatus();
+ Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
+
+ sinon.stub(component, "getPanelStatus", () => Promise.resolve("mock-panelId"));
+ sinon.spy(deckStore, "selectPanel");
+ yield component.updatePanel();
+ Assert.ok(deckStore.selectPanel.calledWith("mock-panelId"));
+});
+
+add_task(function* testActions() {
+ let windowMock = {
+ openUILink() {},
+ };
+ let chromeWindowMock = {
+ gSyncUI: {
+ openSetup() {}
+ }
+ };
+ sinon.spy(windowMock, "openUILink");
+ sinon.spy(chromeWindowMock.gSyncUI, "openSetup");
+
+ let getChromeWindowMock = sinon.stub();
+ getChromeWindowMock.returns(chromeWindowMock);
+
+ let component = new SyncedTabsDeckComponent({
+ window: windowMock,
+ getChromeWindowMock
+ });
+
+ let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar";
+ component.openAndroidLink("mock-event");
+ Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
+
+ href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
+ component.openiOSLink("mock-event");
+ Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
+
+ component.openSyncPrefs();
+ Assert.ok(getChromeWindowMock.calledWith(windowMock));
+ Assert.ok(chromeWindowMock.gSyncUI.openSetup.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 000000000..69abb4024
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckStore.js
@@ -0,0 +1,64 @@
+"use strict";
+
+let { SyncedTabsDeckStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckStore.js", {});
+
+add_task(function* testSelectUnkownPanel() {
+ let deckStore = new SyncedTabsDeckStore();
+ let spy = sinon.spy();
+
+ deckStore.on("change", spy);
+ deckStore.selectPanel("foo");
+
+ Assert.ok(!spy.called);
+});
+
+add_task(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(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(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 000000000..51580235f
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsListStore.js
@@ -0,0 +1,266 @@
+"use strict";
+
+let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
+let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
+
+const FIXTURE = [
+ {
+ "id": "2xU5h-4bkWqA",
+ "type": "client",
+ "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",
+ "name": "desktop",
+ "isMobile": false,
+ "tabs": []
+ }
+];
+
+add_task(function* testGetDataEmpty() {
+ let store = new SyncedTabsListStore(SyncedTabs);
+ let spy = sinon.spy();
+
+ sinon.stub(SyncedTabs, "getTabClients", () => {
+ return Promise.resolve([]);
+ });
+ store.on("change", spy);
+
+ yield store.getData();
+
+ Assert.ok(SyncedTabs.getTabClients.calledWith(""));
+ Assert.ok(spy.calledWith({
+ clients: [],
+ canUpdateAll: false,
+ canUpdateInput: false,
+ filter: "",
+ inputFocused: false
+ }));
+
+ yield 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(function* testRowSelectionWithoutFilter() {
+ let store = new SyncedTabsListStore(SyncedTabs);
+ let spy = sinon.spy();
+
+ sinon.stub(SyncedTabs, "getTabClients", () => {
+ return Promise.resolve(FIXTURE);
+ });
+
+ yield 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(function* testToggleBranches() {
+ let store = new SyncedTabsListStore(SyncedTabs);
+ let spy = sinon.spy();
+
+ sinon.stub(SyncedTabs, "getTabClients", () => {
+ return Promise.resolve(FIXTURE);
+ });
+
+ yield 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(function* testRowSelectionWithFilter() {
+ let store = new SyncedTabsListStore(SyncedTabs);
+ let spy = sinon.spy();
+
+ sinon.stub(SyncedTabs, "getTabClients", () => {
+ return Promise.resolve(FIXTURE);
+ });
+
+ yield 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(function* testFilterAndClearFilter() {
+ let store = new SyncedTabsListStore(SyncedTabs);
+ let spy = sinon.spy();
+
+ sinon.stub(SyncedTabs, "getTabClients", () => {
+ return Promise.resolve(FIXTURE);
+ });
+ store.on("change", spy);
+
+ yield 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");
+
+ yield 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(function* testFocusBlurInput() {
+ let store = new SyncedTabsListStore(SyncedTabs);
+ let spy = sinon.spy();
+
+ sinon.stub(SyncedTabs, "getTabClients", () => {
+ return Promise.resolve(FIXTURE);
+ });
+ store.on("change", spy);
+
+ yield 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 000000000..0b0665a1b
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
@@ -0,0 +1,155 @@
+"use strict";
+
+let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
+let { TabListComponent } = Cu.import("resource:///modules/syncedtabs/TabListComponent.js", {});
+let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
+let { View } = Cu.import("resource:///modules/syncedtabs/TabListView.js", {});
+
+const ACTION_METHODS = [
+ "onSelectRow",
+ "onOpenTab",
+ "onOpenTabs",
+ "onMoveSelectionDown",
+ "onMoveSelectionUp",
+ "onToggleBranch",
+ "onBookmarkTab",
+ "onSyncRefresh",
+ "onFilter",
+ "onClearFilter",
+ "onFilterFocus",
+ "onFilterBlur",
+];
+
+add_task(function* testInitUninit() {
+ let store = new SyncedTabsListStore();
+ let ViewMock = sinon.stub();
+ let view = {render() {}, destroy() {}};
+
+ 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, 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(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(); }
+ },
+ PlacesUtils: { bookmarksMenuFolderId: "id" }
+ },
+ getBrowserURL() {},
+ openDialog() {},
+ openUILinkIn() {}
+ };
+ 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][1], "uri");
+ Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][2], "title");
+
+ sinon.spy(windowMock, "openUILinkIn");
+ component.onOpenTab("uri", "where", "params");
+ Assert.ok(windowMock.openUILinkIn.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, false, false));
+ component.onOpenTabs(tabsToOpen, "tabshifted");
+ Assert.ok(chromeWindowMock.gBrowser.loadTabs.calledWith(tabsToOpen, true, false));
+
+ 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 000000000..1cb8dcb7a
--- /dev/null
+++ b/browser/components/syncedtabs/test/xpcshell/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head = head.js
+tail =
+firefox-appdir = browser
+
+[test_EventEmitter.js]
+[test_SyncedTabsDeckStore.js]
+[test_SyncedTabsListStore.js]
+[test_SyncedTabsDeckComponent.js]
+[test_TabListComponent.js]