Bug 1210586 - Create a Synced tabs sidebar r=markh

This commit is contained in:
Zachary Carter 2016-01-27 14:40:30 -08:00
Родитель 9ec51cfd62
Коммит 4937ef326e
48 изменённых файлов: 9219 добавлений и 5 удалений

Просмотреть файл

@ -17,6 +17,7 @@ const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
'menu_socialSidebar',
'menu_historySidebar',
'menu_bookmarksSidebar',
'menu_tabsSidebar',
];
function isSidebarShowing(window) {

Просмотреть файл

@ -242,7 +242,9 @@
key="key_gotoHistory"
observes="viewHistorySidebar"
label="&historyButton.label;"/>
<menuitem id="menu_tabsSidebar"
observes="viewTabsSidebar"
label="&syncedTabs.sidebar.label;"/>
<!-- Service providers with sidebars are inserted between these two menuseperators -->
<menuseparator hidden="true"/>
<menuseparator class="social-provider-menu" hidden="true"/>

Просмотреть файл

@ -188,6 +188,10 @@
<broadcaster id="sync-setup-state"/>
<broadcaster id="sync-syncnow-state" hidden="true"/>
<broadcaster id="sync-reauth-state" hidden="true"/>
<broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
type="checkbox" group="sidebar"
sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml"
oncommand="SidebarUI.toggle('viewTabsSidebar');"/>
<broadcaster id="workOfflineMenuitemState"/>
<broadcaster id="socialSidebarBroadcaster" hidden="true"/>

Просмотреть файл

@ -191,7 +191,7 @@ var SidebarUI = {
return new Promise((resolve, reject) => {
let sidebarBroadcaster = document.getElementById(commandID);
if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
reject(new Error("Invalid sidebar broadcaster specified"));
reject(new Error("Invalid sidebar broadcaster specified: " + commandID));
return;
}

Просмотреть файл

@ -474,6 +474,19 @@
acceskey="&emeNotificationsDontAskAgain.accesskey;"
oncommand="gEMEHandler.onDontAskAgain(this);"/>
</menupopup>
<menupopup id="SyncedTabsSidebarContext">
<menuitem label="&syncedTabs.context.openTab.label;"
accesskey="&syncedTabs.context.openTab.accesskey;"
id="syncedTabsOpenSelected"/>
<menuitem label="&syncedTabs.context.bookmarkSingleTab.label;"
accesskey="&syncedTabs.context.bookmarkSingleTab.accesskey;"
id="syncedTabsBookmarkSelected"/>
<menuseparator/>
<menuitem label="&syncedTabs.context.refreshList.label;"
accesskey="&syncedTabs.context.refreshList.accesskey;"
id="syncedTabsRefresh"/>
</menupopup>
</popupset>
#ifdef CAN_DRAW_IN_TITLEBAR

Просмотреть файл

@ -112,6 +112,10 @@
<!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
<!-- When Sync is ready to sync -->
<vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state">
<toolbarbutton id="PanelUI-remotetabs-view-sidebar"
class="subviewbutton"
observes="viewTabsSidebar"
label="&appMenuRemoteTabs.sidebar.label;"/>
<toolbarbutton id="PanelUI-remotetabs-syncnow"
observes="sync-status"
class="subviewbutton"

Просмотреть файл

@ -21,6 +21,7 @@ DIRS += [
'sessionstore',
'shell',
'selfsupport',
'syncedtabs',
'uitour',
'translation',
]

Просмотреть файл

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = [
"EventEmitter"
];
// Simple event emitter abstraction for storage objects to use.
function EventEmitter () {
this._events = new Map();
}
EventEmitter.prototype = {
on(event, listener) {
if (this._events.has(event)) {
this._events.get(event).add(listener);
} else {
this._events.set(event, new Set([listener]));
}
},
off(event, listener) {
if (!this._events.has(event)) {
return;
}
this._events.get(event).delete(listener);
},
emit(event, ...args) {
if (!this._events.has(event)) {
return;
}
for (let listener of this._events.get(event).values()) {
try {
listener.apply(this, args);
} catch (e) {
Cu.reportError(e);
}
}
},
};

Просмотреть файл

@ -0,0 +1,165 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckStore.js");
Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckView.js");
Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js");
Cu.import("resource:///modules/syncedtabs/TabListComponent.js");
Cu.import("resource:///modules/syncedtabs/TabListView.js");
let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
});
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"SyncedTabsDeckComponent"
];
/* SyncedTabsDeckComponent
* This component instantiates views and storage objects as well as defines
* behaviors that will be passed down to the views. This helps keep the views
* isolated and easier to test.
*/
function SyncedTabsDeckComponent({
window, SyncedTabs, fxAccounts, deckStore, listStore, listComponent, DeckView, getChromeWindowMock,
}) {
this._window = window;
this._SyncedTabs = SyncedTabs;
this._fxAccounts = fxAccounts;
this._DeckView = DeckView || SyncedTabsDeckView;
// used to stub during tests
this._getChromeWindow = getChromeWindowMock || getChromeWindow;
this._deckStore = deckStore || new SyncedTabsDeckStore();
this._syncedTabsListStore = listStore || new SyncedTabsListStore(SyncedTabs);
this.tabListComponent = listComponent || new TabListComponent({
window: this._window,
store: this._syncedTabsListStore,
View: TabListView,
SyncedTabs: SyncedTabs
});
};
SyncedTabsDeckComponent.prototype = {
PANELS: {
TABS_CONTAINER: "tabs-container",
TABS_FETCHING: "tabs-fetching",
NOT_AUTHED_INFO: "notAuthedInfo",
SINGLE_DEVICE_INFO: "singleDeviceInfo",
TABS_DISABLED: "tabs-disabled",
},
get container() {
return this._deckView ? this._deckView.container : null;
},
init() {
Services.obs.addObserver(this, this._SyncedTabs.TOPIC_TABS_CHANGED, false);
Services.obs.addObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION, false);
// Go ahead and trigger sync
this._SyncedTabs.syncTabs()
.catch(Cu.reportError);
this._deckView = new this._DeckView(this._window, this.tabListComponent, {
onAndroidClick: event => this.openAndroidLink(event),
oniOSClick: event => this.openiOSLink(event),
onSyncPrefClick: event => this.openSyncPrefs(event)
});
this._deckStore.on("change", state => this._deckView.render(state));
// Trigger the initial rendering of the deck view
this._deckStore.setPanels(Object.values(this.PANELS));
// Set the initial panel to display
this.updatePanel();
},
uninit() {
Services.obs.removeObserver(this, this._SyncedTabs.TOPIC_TABS_CHANGED);
Services.obs.removeObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION);
this._deckView.destroy();
},
observe(subject, topic, data) {
switch (topic) {
case this._SyncedTabs.TOPIC_TABS_CHANGED:
this._syncedTabsListStore.getData();
this.updatePanel();
break;
case FxAccountsCommon.ONLOGIN_NOTIFICATION:
this.updatePanel();
break;
default:
break;
}
},
// There's no good way to mock fxAccounts in browser tests where it's already
// been instantiated, so we have this method for stubbing.
_accountStatus() {
return this._fxAccounts.accountStatus();
},
getPanelStatus() {
return this._accountStatus().then(exists => {
if (!exists) {
return this.PANELS.NOT_AUTHED_INFO;
}
if (!this._SyncedTabs.isConfiguredToSyncTabs) {
return this.PANELS.TABS_DISABLED;
}
if (!this._SyncedTabs.hasSyncedThisSession) {
return this.PANELS.TABS_FETCHING;
}
return this._SyncedTabs.getTabClients().then(clients => {
if (clients.length) {
return this.PANELS.TABS_CONTAINER;
}
return this.PANELS.SINGLE_DEVICE_INFO;
});
})
.catch(err => {
Cu.reportError(err);
return this.PANELS.NOT_AUTHED_INFO;
});
},
updatePanel() {
// return promise for tests
return this.getPanelStatus()
.then(panelId => this._deckStore.selectPanel(panelId))
.catch(Cu.reportError);
},
openAndroidLink(event) {
let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar";
this._openUrl(href, event);
},
openiOSLink(event) {
let href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
this._openUrl(href, event);
},
_openUrl(url, event) {
this._window.openUILink(url, event);
},
openSyncPrefs() {
this._getChromeWindow(this._window).gSyncUI.openSetup();
}
};

Просмотреть файл

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {});
this.EXPORTED_SYMBOLS = [
"SyncedTabsDeckStore"
];
/**
* SyncedTabsDeckStore
*
* This store keeps track of the deck view state, including the panels and which
* one is selected. The view listens for change events on the store, which are
* triggered whenever the state changes. If it's a small change, the state
* will have `isUpdatable` set to true so the view can skip rerendering the whole
* DOM.
*/
function SyncedTabsDeckStore() {
EventEmitter.call(this);
this._panels = [];
};
Object.assign(SyncedTabsDeckStore.prototype, EventEmitter.prototype, {
_change(isUpdatable = false) {
let panels = this._panels.map(panel => {
return {id: panel, selected: panel === this._selectedPanel};
});
this.emit("change", {panels, isUpdatable: isUpdatable});
},
/**
* Sets the selected panelId and triggers a change event.
* @param {String} panelId - ID of the panel to select.
*/
selectPanel(panelId) {
if (this._panels.indexOf(panelId) === -1 || this._selectedPanel === panelId) {
return;
}
this._selectedPanel = panelId;
this._change(true);
},
/**
* Update the set of panels in the deck and trigger a change event.
* @param {Array} panels - an array of IDs for each panel in the deck.
*/
setPanels(panels) {
if (panels === this._panels) {
return;
}
this._panels = panels || [];
this._change();
}
});

Просмотреть файл

@ -0,0 +1,111 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"SyncedTabsDeckView"
];
/**
* SyncedTabsDeckView
*
* Instances of SyncedTabsDeckView render DOM nodes from a given state.
* No state is kept internaly and the DOM will completely
* rerender unless the state flags `isUpdatable`, which helps
* make small changes without the overhead of a full rerender.
*/
const SyncedTabsDeckView = function (window, tabListComponent, props) {
this.props = props;
this._window = window;
this._doc = window.document;
this._tabListComponent = tabListComponent;
this._deckTemplate = this._doc.getElementById("deck-template");
this.container = this._doc.createElement("div");
};
SyncedTabsDeckView.prototype = {
render(state) {
if (state.isUpdatable) {
this.update(state);
} else {
this.create(state);
}
},
create(state) {
let deck = this._doc.importNode(this._deckTemplate.content, true).firstElementChild;
this._clearChilden();
let tabListWrapper = this._doc.createElement("div");
tabListWrapper.className = "tabs-container sync-state";
this._tabListComponent.init();
tabListWrapper.appendChild(this._tabListComponent.container);
deck.appendChild(tabListWrapper);
this.container.appendChild(deck);
this._generateDevicePromo();
this._attachListeners();
this.update(state);
},
_getBrowserBundle() {
return getChromeWindow(this._window).document.getElementById("bundle_browser");
},
_generateDevicePromo() {
let bundle = this._getBrowserBundle();
let formatArgs = ["android", "ios"].map(os => {
let link = this._doc.createElement("a");
link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`)
link.className = `${os}-link text-link`;
link.setAttribute("href", "#");
return link.outerHTML;
});
// Put it all together...
let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo", formatArgs);
this.container.querySelector(".device-promo").innerHTML = contents;
},
destroy() {
this._tabListComponent.uninit();
this.container.remove();
},
update(state) {
for (let panel of state.panels) {
if (panel.selected) {
this.container.getElementsByClassName(panel.id).item(0).classList.add("selected");
} else {
this.container.getElementsByClassName(panel.id).item(0).classList.remove("selected");
}
}
},
_clearChilden() {
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
},
_attachListeners() {
this.container.querySelector(".android-link").addEventListener("click", this.props.onAndroidClick);
this.container.querySelector(".ios-link").addEventListener("click", this.props.oniOSClick);
let syncPrefLinks = this.container.querySelectorAll(".sync-prefs");
for (let link of syncPrefLinks) {
link.addEventListener("click", this.props.onSyncPrefClick);
}
},
};

Просмотреть файл

@ -0,0 +1,228 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {});
this.EXPORTED_SYMBOLS = [
"SyncedTabsListStore"
];
/**
* SyncedTabsListStore
*
* Instances of this store encapsulate all of the state associated with a synced tabs list view.
* The state includes the clients, their tabs, the row that is currently selected,
* and the filtered query.
*/
function SyncedTabsListStore(SyncedTabs) {
EventEmitter.call(this);
this._SyncedTabs = SyncedTabs;
this.data = [];
this._closedClients = {};
this._selectedRow = [-1, -1];
this.filter = "";
this.inputFocused = false;
};
Object.assign(SyncedTabsListStore.prototype, EventEmitter.prototype, {
// This internal method triggers the "change" event that views
// listen for. It denormalizes the state so that it's easier for
// the view to deal with. updateType hints to the view what
// actually needs to be rerendered or just updated, and can be
// empty (to (re)render everything), "searchbox" (to rerender just the tab list),
// or "all" (to skip rendering and just update all attributes of existing nodes).
_change(updateType) {
let selectedParent = this._selectedRow[0];
let selectedChild = this._selectedRow[1];
let rowSelected = false;
// clone the data so that consumers can't mutate internal storage
let data = Cu.cloneInto(this.data, {});
let tabCount = 0;
data.forEach((client, index) => {
client.closed = !!this._closedClients[client.id];
if (rowSelected || selectedParent < 0) {
return;
}
if (this.filter) {
if (selectedParent < tabCount + client.tabs.length) {
client.tabs[selectedParent - tabCount].selected = true;
client.tabs[selectedParent - tabCount].focused = !this.inputFocused;
rowSelected = true;
} else {
tabCount += client.tabs.length;
}
return;
}
if (selectedParent === index && selectedChild === -1) {
client.selected = true;
client.focused = !this.inputFocused;
rowSelected = true;
} else if (selectedParent === index) {
client.tabs[selectedChild].selected = true;
client.tabs[selectedChild].focused = !this.inputFocused;
rowSelected = true;
}
});
// If this were React the view would be smart enough
// to not re-render the whole list unless necessary. But it's
// not, so updateType is a hint to the view of what actually
// needs to be rerendered.
this.emit("change", {
clients: data,
canUpdateAll: updateType === "all",
canUpdateInput: updateType === "searchbox",
filter: this.filter,
inputFocused: this.inputFocused
});
},
/**
* Moves the row selection from a child to its parent,
* which occurs when the parent of a selected row closes.
*/
_selectParentRow() {
this._selectedRow[1] = -1;
},
_toggleBranch(id, closed) {
this._closedClients[id] = closed;
if (this._closedClients[id]) {
this._selectParentRow();
}
this._change("all");
},
_isOpen(client) {
return !this._closedClients[client.id];
},
moveSelectionDown() {
let branchRow = this._selectedRow[0];
let childRow = this._selectedRow[1];
let branch = this.data[branchRow];
if (this.filter) {
this.selectRow(branchRow + 1);
return;
}
if (branchRow < 0) {
this.selectRow(0, -1);
} else if ((!branch.tabs.length || childRow >= branch.tabs.length - 1 || !this._isOpen(branch)) && branchRow < this.data.length) {
this.selectRow(branchRow + 1, -1);
} else if(childRow < branch.tabs.length) {
this.selectRow(branchRow, childRow + 1);
}
},
moveSelectionUp() {
let branchRow = this._selectedRow[0];
let childRow = this._selectedRow[1];
let branch = this.data[branchRow];
if (this.filter) {
this.selectRow(branchRow - 1);
return;
}
if (branchRow < 0) {
this.selectRow(0, -1);
} else if (childRow < 0 && branchRow > 0) {
let prevBranch = this.data[branchRow - 1];
let newChildRow = this._isOpen(prevBranch) ? prevBranch.tabs.length - 1 : -1;
this.selectRow(branchRow - 1, newChildRow);
} else if (childRow >= 0) {
this.selectRow(branchRow, childRow - 1);
}
},
// Selects a row and makes sure the selection is within bounds
selectRow(parent, child) {
let maxParentRow = this.filter ? this._tabCount() : this.data.length;
let parentRow = parent;
if (parent <= -1) {
parentRow = 0;
} else if (parent >= maxParentRow) {
parentRow = maxParentRow - 1;
}
let childRow = child;
if (parentRow === -1 || this.filter || typeof child === "undefined" || child < -1) {
childRow = -1;
} else if (child >= this.data[parentRow].tabs.length) {
childRow = this.data[parentRow].tabs.length - 1;
}
if (this._selectedRow[0] === parentRow && this._selectedRow[1] === childRow) {
return;
}
this._selectedRow = [parentRow, childRow];
this.inputFocused = false;
this._change("all");
},
_tabCount() {
return this.data.reduce((prev, curr) => curr.tabs.length + prev, 0);
},
toggleBranch(id) {
this._toggleBranch(id, !this._closedClients[id]);
},
closeBranch(id) {
this._toggleBranch(id, true);
},
openBranch(id) {
this._toggleBranch(id, false);
},
focusInput() {
this.inputFocused = true;
this._change("update");
},
blurInput() {
this.inputFocused = false;
this._change("update");
},
clearFilter() {
this.filter = "";
this._selectedRow = [-1, -1];
return this.getData();
},
// Fetches data from the SyncedTabs module and triggers
// and update
getData(filter) {
let updateType;
if (typeof filter !== "undefined") {
this.filter = filter;
this._selectedRow = [-1, -1];
// When a filter is specified we tell the view that only the list
// needs to be rerendered so that it doesn't disrupt the input
// field's focus.
updateType = "searchbox";
}
// return promise for tests
return this._SyncedTabs.getTabClients(this.filter)
.then(result => {
this.data = result;
this._change(updateType);
})
.catch(Cu.reportError);
}
});

Просмотреть файл

@ -0,0 +1,114 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"TabListComponent"
];
/**
* TabListComponent
*
* The purpose of this component is to compose the view, state, and actions.
* It defines high level actions that act on the state and passes them to the
* view for it to trigger during user interaction. It also subscribes the view
* to state changes so it can rerender.
*/
function TabListComponent({window, store, View, SyncedTabs}) {
this._window = window;
this._store = store;
this._View = View;
// used to trigger Sync from context menu
this._SyncedTabs = SyncedTabs;
}
TabListComponent.prototype = {
get container() {
return this._view.container;
},
init() {
log.debug("Initializing TabListComponent");
this._view = new this._View(this._window, {
onSelectRow: (...args) => this.onSelectRow(...args),
onOpenTab: (...args) => this.onOpenTab(...args),
onMoveSelectionDown: (...args) => this.onMoveSelectionDown(...args),
onMoveSelectionUp: (...args) => this.onMoveSelectionUp(...args),
onToggleBranch: (...args) => this.onToggleBranch(...args),
onBookmarkTab: (...args) => this.onBookmarkTab(...args),
onSyncRefresh: (...args) => this.onSyncRefresh(...args),
onFilter: (...args) => this.onFilter(...args),
onClearFilter: (...args) => this.onClearFilter(...args),
onFilterFocus: (...args) => this.onFilterFocus(...args),
onFilterBlur: (...args) => this.onFilterBlur(...args)
});
this._store.on("change", state => this._view.render(state));
this._view.render({clients: []});
// get what's already available...
this._store.getData();
this._store.focusInput();
},
uninit() {
this._view.destroy();
},
onFilter(query) {
this._store.getData(query);
},
onClearFilter() {
this._store.clearFilter();
},
onFilterFocus() {
this._store.focusInput();
},
onFilterBlur() {
this._store.blurInput();
},
onSelectRow(position, id) {
this._store.selectRow(position[0], position[1]);
if (id) {
this._store.toggleBranch(id);
}
},
onMoveSelectionDown() {
this._store.moveSelectionDown();
},
onMoveSelectionUp() {
this._store.moveSelectionUp();
},
onToggleBranch(id) {
this._store.toggleBranch(id);
},
onBookmarkTab(uri, title) {
this._window.top.PlacesCommandHook
.bookmarkLink(this._window.PlacesUtils.bookmarksMenuFolderId, uri, title)
.catch(Cu.reportError);
},
onOpenTab(url, event) {
this._window.openUILink(url, event);
},
onSyncRefresh() {
this._SyncedTabs.syncTabs(true);
}
};

Просмотреть файл

@ -0,0 +1,438 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
this.EXPORTED_SYMBOLS = [
"TabListView"
];
function getContextMenu(window) {
return getChromeWindow(window).document.getElementById("SyncedTabsSidebarContext");
}
/*
* TabListView
*
* Given a state, this object will render the corresponding DOM.
* It maintains no state of it's own. It listens for DOM events
* and triggers actions that may cause the state to change and
* ultimately the view to rerender.
*/
function TabListView(window, props) {
this.props = props;
this._window = window;
this._doc = this._window.document;
this._tabsContainerTemplate = this._doc.getElementById("tabs-container-template");
this._clientTemplate = this._doc.getElementById("client-template");
this._emptyClientTemplate = this._doc.getElementById("empty-client-template");
this._tabTemplate = this._doc.getElementById("tab-template");
this.container = this._doc.createElement("div");
this._setupContextMenu();
};
TabListView.prototype = {
render(state) {
// Don't rerender anything; just update attributes, e.g. selection
if (state.canUpdateAll) {
this._update(state);
return;
}
// Rerender the tab list
if (state.canUpdateInput) {
this._updateSearchBox(state);
this._createList(state);
return;
}
// Create the world anew
this._create(state);
},
// Create the initial DOM from templates
_create(state) {
let wrapper = this._doc.importNode(this._tabsContainerTemplate.content, true).firstElementChild;
this._clearChilden();
this.container.appendChild(wrapper);
this.tabsFilter = this.container.querySelector(".tabsFilter");
this.clearFilter = this.container.querySelector(".textbox-search-clear");
this.searchBox = this.container.querySelector(".search-box");
this.list = this.container.querySelector(".list");
this.searchIcon = this.container.querySelector(".textbox-search-icon");
if (state.filter) {
this.tabsFilter.value = state.filter;
}
this._createList(state);
this._updateSearchBox(state);
this._attachListeners();
},
_createList(state) {
this._clearChilden(this.list);
for (let client of state.clients) {
if (state.filter) {
this._renderFilteredClient(client);
} else {
this._renderClient(client);
}
}
},
destroy() {
this._teardownContextMenu();
this.container.remove();
},
_update(state) {
this._updateSearchBox(state);
for (let client of state.clients) {
let clientNode = this._doc.getElementById("item-" + client.id);
if (clientNode) {
this._updateClient(client, clientNode);
}
client.tabs.forEach((tab, index) => {
let tabNode = this._doc.getElementById('tab-' + client.id + '-' + index);
this._updateTab(tab, tabNode, index);
});
}
},
// Client rows are hidden when the list is filtered
_renderFilteredClient(client, filter) {
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
this.list.appendChild(node);
});
},
_renderClient(client) {
let itemNode = client.tabs.length ?
this._createClient(client) :
this._createEmptyClient(client);
this._updateClient(client, itemNode);
let tabsList = itemNode.querySelector(".item-tabs-list");
client.tabs.forEach((tab, index) => {
let node = this._renderTab(client, tab, index);
tabsList.appendChild(node);
});
this.list.appendChild(itemNode);
return itemNode;
},
_renderTab(client, tab, index) {
let itemNode = this._createTab(tab);
this._updateTab(tab, itemNode, index);
return itemNode;
},
_createClient(item) {
return this._doc.importNode(this._clientTemplate.content, true).firstElementChild;
},
_createEmptyClient(item) {
return this._doc.importNode(this._emptyClientTemplate.content, true).firstElementChild;
},
_createTab(item) {
return this._doc.importNode(this._tabTemplate.content, true).firstElementChild;
},
_clearChilden(node) {
let parent = node || this.container;
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
},
_attachListeners() {
this.list.addEventListener("click", this.onClick.bind(this));
this.list.addEventListener("keydown", this.onKeyDown.bind(this));
this.tabsFilter.addEventListener("input", this.onFilter.bind(this));
this.tabsFilter.addEventListener("focus", this.onFilterFocus.bind(this));
this.tabsFilter.addEventListener("blur", this.onFilterBlur.bind(this));
this.clearFilter.addEventListener("click", this.onClearFilter.bind(this));
this.searchIcon.addEventListener("click", this.onFilterFocus.bind(this));
},
_updateSearchBox(state) {
if (state.filter) {
this.searchBox.classList.add("filtered");
} else {
this.searchBox.classList.remove("filtered");
}
if (state.inputFocused) {
this.searchBox.setAttribute("focused", true);
this.tabsFilter.focus();
} else {
this.searchBox.removeAttribute("focused");
}
},
/**
* Update the element representing an item, ensuring it's in sync with the
* underlying data.
* @param {client} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
_updateClient(item, itemNode) {
itemNode.setAttribute("id", "item-" + item.id);
itemNode.setAttribute("title", item.name);
if (item.closed) {
itemNode.classList.add("closed");
} else {
itemNode.classList.remove("closed");
}
if (item.selected) {
itemNode.classList.add("selected");
} else {
itemNode.classList.remove("selected");
}
if (item.focused) {
itemNode.focus();
}
itemNode.dataset.id = item.id;
itemNode.querySelector(".item-title").textContent = item.name;
let icon = itemNode.querySelector(".item-icon-container");
icon.style.backgroundImage = "url(" + item.icon + ")";
},
/**
* Update the element representing a tab, ensuring it's in sync with the
* underlying data.
* @param {tab} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
_updateTab(item, itemNode, index) {
itemNode.setAttribute("title", `${item.title}\n${item.url}`);
itemNode.setAttribute("id", "tab-" + item.client + '-' + index);
if (item.selected) {
itemNode.classList.add("selected");
} else {
itemNode.classList.remove("selected");
}
if (item.focused) {
itemNode.focus();
}
itemNode.dataset.url = item.url;
itemNode.querySelector(".item-title").textContent = item.title;
let icon = itemNode.querySelector(".item-icon-container");
icon.style.backgroundImage = "url(" + item.icon + ")";
},
onClick(event) {
let itemNode = this._findParentItemNode(event.target);
if (!itemNode) {
return;
}
if (itemNode.classList.contains("tab")) {
let url = itemNode.dataset.url;
if (url) {
this.props.onOpenTab(url, event);
}
}
if (event.target.classList.contains("item-twisty-container")) {
this.props.onToggleBranch(itemNode.dataset.id);
return;
}
this._selectRow(itemNode);
},
_selectRow(itemNode) {
this.props.onSelectRow(this._getSelectionPosition(itemNode), itemNode.dataset.id);
},
/**
* Handle a keydown event on the list box.
* @param {Event} event - Triggering event.
*/
onKeyDown(event) {
if (event.keyCode == this._window.KeyEvent.DOM_VK_DOWN) {
event.preventDefault();
this.props.onMoveSelectionDown();
} else if (event.keyCode == this._window.KeyEvent.DOM_VK_UP) {
event.preventDefault();
this.props.onMoveSelectionUp();
} else if (event.keyCode == this._window.KeyEvent.DOM_VK_RETURN) {
let selectedNode = this.container.querySelector('.item.selected');
if (selectedNode.dataset.url) {
this.props.onOpenTab(selectedNode.dataset.url, event);
} else if (selectedNode) {
this.props.onToggleBranch(selectedNode.dataset.id);
}
}
},
onBookmarkTab() {
let item = this.container.querySelector('.item.selected');
if (!item || !item.dataset.url) {
return;
}
let uri = item.dataset.url;
let title = item.querySelector(".item-title").textContent;
this.props.onBookmarkTab(uri, title);
},
onOpenSelected(event) {
let item = this.container.querySelector('.item.selected');
if (this._isTab(item) && item.dataset.url) {
this.props.onOpenTab(item.dataset.url, event);
}
},
onFilter(event) {
let query = event.target.value;
this.props.onFilter(query);
},
onClearFilter() {
this.props.onClearFilter();
},
onFilterFocus() {
this.props.onFilterFocus();
},
onFilterBlur() {
this.props.onFilterBlur();
},
// Set up the custom context menu
_setupContextMenu() {
this._handleContentContextMenu = event =>
this.handleContentContextMenu(event);
this._handleContentContextMenuCommand = event =>
this.handleContentContextMenuCommand(event);
Services.els.addSystemEventListener(this._window, "contextmenu", this._handleContentContextMenu, false);
let menu = getContextMenu(this._window);
menu.addEventListener("command", this._handleContentContextMenuCommand, true);
},
_teardownContextMenu() {
// Tear down context menu
Services.els.removeSystemEventListener(this._window, "contextmenu", this._handleContentContextMenu, false);
let menu = getContextMenu(this._window);
menu.removeEventListener("command", this._handleContentContextMenuCommand, true);
},
handleContentContextMenuCommand(event) {
let id = event.target.getAttribute("id");
switch (id) {
case "syncedTabsOpenSelected":
this.onOpenSelected(event);
break;
case "syncedTabsBookmarkSelected":
this.onBookmarkTab();
break;
case "syncedTabsRefresh":
this.props.onSyncRefresh();
break;
}
},
handleContentContextMenu(event) {
let itemNode = this._findParentItemNode(event.target);
if (itemNode) {
this._selectRow(itemNode);
}
let menu = getContextMenu(this._window);
this.adjustContextMenu(menu);
menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
},
adjustContextMenu(menu) {
let item = this.container.querySelector('.item.selected');
let showTabOptions = this._isTab(item);
let el = menu.firstChild;
while (el) {
if (showTabOptions || el.getAttribute("id") === "syncedTabsRefresh") {
el.hidden = false;
} else {
el.hidden = true;
}
el = el.nextSibling;
}
},
/**
* Find the parent item element, from a given child element.
* @param {Element} node - Child element.
* @return {Element} Element for the item, or null if not found.
*/
_findParentItemNode(node) {
while (node && node !== this.list && node !== this._doc.documentElement &&
!node.classList.contains("item")) {
node = node.parentNode;
}
if (node !== this.list && node !== this._doc.documentElement) {
return node;
}
return null;
},
_findParentBranchNode(node) {
while (node && !node.classList.contains("list") && node !== this._doc.documentElement &&
!node.parentNode.classList.contains("list")) {
node = node.parentNode;
}
if (node !== this.list && node !== this._doc.documentElement) {
return node;
}
return null;
},
_getSelectionPosition(itemNode) {
let parent = this._findParentBranchNode(itemNode);
let parentPosition = this._indexOfNode(parent.parentNode, parent);
let childPosition = -1;
// if the node is not a client, find its position within the parent
if (parent !== itemNode) {
childPosition = this._indexOfNode(itemNode.parentNode, itemNode);
}
return [parentPosition, childPosition];
},
_indexOfNode(parent, child) {
return Array.prototype.indexOf.call(parent.childNodes, child);
},
_isTab(item) {
return item && item.classList.contains("tab");
}
};

Просмотреть файл

@ -0,0 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/syncedtabs/sidebar.xhtml
content/browser/syncedtabs/sidebar.js

Просмотреть файл

@ -0,0 +1,24 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
JAR_MANIFESTS += ['jar.mn']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
EXTRA_JS_MODULES.syncedtabs += [
'EventEmitter.jsm',
'SyncedTabsDeckComponent.js',
'SyncedTabsDeckStore.js',
'SyncedTabsDeckView.js',
'SyncedTabsListStore.js',
'TabListComponent.js',
'TabListView.js',
'util.js',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Synced tabs')

Просмотреть файл

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/SyncedTabs.jsm");
Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckComponent.js");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
this.syncedTabsDeckComponent = new SyncedTabsDeckComponent({window, SyncedTabs, fxAccounts});
let onLoaded = () => {
syncedTabsDeckComponent.init();
document.body.appendChild(syncedTabsDeckComponent.container);
};
let onUnloaded = () => {
removeEventListener("DOMContentLoaded", onLoaded);
removeEventListener("unload", onUnloaded);
syncedTabsDeckComponent.uninit();
};
addEventListener("DOMContentLoaded", onLoaded);
addEventListener("unload", onUnloaded);

Просмотреть файл

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xml:lang="en" lang="en">
<head>
<script src="chrome://browser/content/syncedtabs/sidebar.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
<link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/syncedtabs/sidebar.css"/>
<link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/"/>
<link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/textbox.css"/>
<title>&syncedTabs.sidebar.label;</title>
</head>
<body role="application">
<template id="client-template">
<div class="item client" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-twisty-container"></div>
<div class="item-icon-container"></div>
<p class="item-title"></p>
</div>
<div class="item-tabs-list"></div>
</div>
</template>
<template id="empty-client-template">
<div class="item empty client" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-twisty-container"></div>
<div class="item-icon-container"></div>
<p class="item-title"></p>
</div>
<div class="item-tabs-list">
<div class="item empty" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-icon-container"></div>
<p class="item-title">&syncedTabs.sidebar.notabs.label;</p>
</div>
</div>
</div>
</div>
</template>
<template id="tab-template">
<div class="item tab" role="option" tabindex="-1">
<div class="item-title-container">
<div class="item-icon-container"></div>
<p class="item-title"></p>
</div>
</div>
</template>
<template id="tabs-container-template">
<div class="tabs-container">
<div class="sidebar-search-container">
<div class="search-box compact">
<div class="textbox-input-box">
<input type="text" class="tabsFilter textbox-input"/>
<div class="textbox-search-icons">
<a class="textbox-search-clear"></a>
<a class="textbox-search-icon"></a>
</div>
</div>
</div>
</div>
<div class="list" role="listbox" tabindex="1"></div>
</div>
</template>
<template id="deck-template">
<div class="deck">
<div class="tabs-fetching sync-state">
<p>&syncedTabs.sidebar.fetching.label;</p>
</div>
<div class="notAuthedInfo sync-state">
<p>&syncedTabs.sidebar.notsignedin.label;</p>
<p><a href="#" class="sync-prefs text-link">&fxaSignIn.label;</a></p>
</div>
<div class="singleDeviceInfo sync-state">
<p>&syncedTabs.sidebar.noclients.label;</p>
<p class="device-promo"></p>
</div>
<div class="tabs-disabled sync-state">
<p>&syncedTabs.sidebar.tabsnotsyncing.label;</p>
<p><a href="#" class="sync-prefs text-link">&syncedTabs.sidebar.openprefs.label;</a></p>
</div>
</div>
</template>
</body>
</html>

Просмотреть файл

@ -0,0 +1,4 @@
[DEFAULT]
support-files = head.js
[browser_sidebar_syncedtabslist.js]

Просмотреть файл

@ -0,0 +1,259 @@
"use strict";
const FIXTURE = [
{
"id": "7cqCr77ptzX3",
"type": "client",
"name": "zcarter's Nightly on MacBook-Pro-25",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"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://mozapps/skin/places/defaultFavicon.png",
"client": "7cqCr77ptzX3",
"lastUsed": 1452124677
}
]
},
{
"id": "2xU5h-4bkWqA",
"type": "client",
"name": "laptop",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"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",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"tabs": []
}
];
let originalSyncedTabsInternal = null;
function* testClean() {
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
syncedTabsDeckComponent._accountStatus.restore();
SyncedTabs._internal.getTabClients.restore();
SyncedTabs._internal = originalSyncedTabsInternal;
let defer = Promise.defer();
window.SidebarUI.browser.contentWindow.addEventListener("unload", defer.resolve);
SidebarUI.hide();
yield defer.promise;
window.SidebarUI.browser.contentWindow.removeEventListener("unload", defer.resolve);
}
add_task(function* testSyncedTabsSidebarList() {
yield SidebarUI.show('viewTabsSidebar');
Assert.equal(SidebarUI.currentID, "viewTabsSidebar", "Sidebar should have SyncedTabs loaded");
let syncedTabsDeckComponent = SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = SidebarUI.browser.contentWindow.SyncedTabs;
Assert.ok(syncedTabsDeckComponent, "component exists");
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: true,
hasSyncedThisSession: true,
getTabClients() { return Promise.resolve([])},
syncTabs() {return Promise.resolve();},
};
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(true));
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve(FIXTURE));
yield 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
yield 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, 3,
"three 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");
Array.prototype.forEach.call(selectedPanel.querySelectorAll(".client"), (clientNode, i) => {
checkItem(clientNode, FIXTURE[i]);
Array.prototype.forEach.call(clientNode.querySelectorAll(".tab"), (tabNode, j) => {
checkItem(tabNode, FIXTURE[i].tabs[j]);
});
});
});
add_task(testClean);
add_task(function* testSyncedTabsSidebarFilteredList() {
yield SidebarUI.show('viewTabsSidebar');
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
Assert.ok(syncedTabsDeckComponent, "component exists");
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: true,
hasSyncedThisSession: true,
getTabClients() { return Promise.resolve([])},
syncTabs() {return Promise.resolve();},
};
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(true));
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve(FIXTURE));
yield 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
yield syncedTabsDeckComponent.tabListComponent._store.getData();
let filterInput = syncedTabsDeckComponent.container.querySelector(".tabsFilter");
filterInput.value = "filter text";
filterInput.blur();
yield 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, 3,
"three 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");
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]);
});
});
add_task(testClean);
add_task(function* testSyncedTabsSidebarStatus() {
let accountExists = false;
yield SidebarUI.show('viewTabsSidebar');
let syncedTabsDeckComponent = window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
let SyncedTabs = window.SidebarUI.browser.contentWindow.SyncedTabs;
originalSyncedTabsInternal = SyncedTabs._internal;
SyncedTabs._internal = {
isConfiguredToSyncTabs: false,
hasSyncedThisSession: false,
getTabClients() {},
syncTabs() {return Promise.resolve();},
};
Assert.ok(syncedTabsDeckComponent, "component exists");
sinon.spy(syncedTabsDeckComponent, "updatePanel");
sinon.spy(syncedTabsDeckComponent, "observe");
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.reject("Test error"));
yield syncedTabsDeckComponent.updatePanel();
let selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("notAuthedInfo"),
"not-authed panel is selected on auth error");
syncedTabsDeckComponent._accountStatus.restore();
sinon.stub(syncedTabsDeckComponent, "_accountStatus", ()=> Promise.resolve(accountExists));
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("notAuthedInfo"),
"not-authed panel is selected");
accountExists = true;
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-disabled"),
"tabs disabled panel is selected");
SyncedTabs._internal.isConfiguredToSyncTabs = true;
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-fetching"),
"tabs fetch panel is selected");
SyncedTabs._internal.hasSyncedThisSession = true;
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve([]));
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("singleDeviceInfo"),
"tabs fetch panel is selected");
SyncedTabs._internal.getTabClients.restore();
sinon.stub(SyncedTabs._internal, "getTabClients", ()=> Promise.resolve([{id: "mock"}]));
yield syncedTabsDeckComponent.updatePanel();
selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
Assert.ok(selectedPanel.classList.contains("tabs-container"),
"tabs panel is selected");
});
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");
}
};

Просмотреть файл

@ -0,0 +1,18 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/docs/
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
registerCleanupFunction(function*() {
// Cleanup window or the test runner will throw an error
delete window.sinon;
delete window.setImmediate;
delete window.clearImmediate;
});

Просмотреть файл

@ -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/
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("resource://testing-common/sinon-1.16.1.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);
});

Просмотреть файл

@ -0,0 +1,217 @@
"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");
Assert.ok(deckStore.setPanels.calledWith(Object.values(component.PANELS)),
"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 listComponent = {};
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);
});

Просмотреть файл

@ -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");
});

Просмотреть файл

@ -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",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"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",
"icon": "chrome://browser/skin/sync-desktopIcon.png",
"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");
});

Просмотреть файл

@ -0,0 +1,129 @@
"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",
"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 windowMock = {
top: {
PlacesCommandHook: {
bookmarkLink() { return Promise.resolve(); }
}
},
openUILink() {},
PlacesUtils: { bookmarksMenuFolderId: "id" },
};
let component = new TabListComponent({
window: windowMock, store, View: null, SyncedTabs});
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");
sinon.stub(store, "toggleBranch");
component.onSelectRow([-1, -1], "foo-id");
Assert.ok(store.selectRow.calledWith(-1, -1));
Assert.ok(store.toggleBranch.calledWith("foo-id"));
sinon.stub(store, "moveSelectionDown");
component.onMoveSelectionDown();
Assert.ok(store.moveSelectionDown.called);
sinon.stub(store, "moveSelectionUp");
component.onMoveSelectionUp();
Assert.ok(store.moveSelectionUp.called);
component.onToggleBranch("foo-id");
Assert.ok(store.toggleBranch.secondCall.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, "openUILink");
component.onOpenTab("uri", "event");
Assert.ok(windowMock.openUILink.calledWith("uri", "event"));
sinon.stub(SyncedTabs, "syncTabs");
component.onSyncRefresh();
Assert.ok(SyncedTabs.syncTabs.calledWith(true));
SyncedTabs.syncTabs.restore();
});

Просмотреть файл

@ -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]

Просмотреть файл

@ -0,0 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = [
"getChromeWindow"
];
// Get the chrome (ie, browser) window hosting this content.
function getChromeWindow(window) {
return window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.wrappedJSObject;
}

Просмотреть файл

@ -421,6 +421,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY appMenuRemoteTabs.openprefs.label "Sync Preferences">
<!ENTITY appMenuRemoteTabs.notsignedin.label "Sign in to view a list of tabs from your other devices.">
<!ENTITY appMenuRemoteTabs.signin.label "Sign in to Sync">
<!ENTITY appMenuRemoteTabs.sidebar.label "View Synced Tabs Sidebar">
<!ENTITY customizeMenu.addToToolbar.label "Add to Toolbar">
<!ENTITY customizeMenu.addToToolbar.accesskey "A">
@ -786,6 +787,24 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!-- LOCALIZATION NOTE (syncTabsMenu3.label): This appears in the history menu and history panel -->
<!ENTITY syncTabsMenu3.label "Synced Tabs">
<!ENTITY syncedTabs.sidebar.label "Synced Tabs">
<!ENTITY syncedTabs.sidebar.fetching.label "Fetching Synced Tabs…">
<!ENTITY syncedTabs.sidebar.noclients.label "Sign in to Firefox from your other devices to view their tabs here.">
<!ENTITY syncedTabs.sidebar.notsignedin.label "Sign in to view a list of tabs from your other devices.">
<!ENTITY syncedTabs.sidebar.notabs.label "No open tabs">
<!ENTITY syncedTabs.sidebar.openprefs.label "Open &syncBrand.shortName.label; Preferences">
<!-- LOCALIZATION NOTE (syncedTabs.sidebar.tabsnotsyncing.label): This is shown
when Sync is configured but syncing tabs is disabled. -->
<!ENTITY syncedTabs.sidebar.tabsnotsyncing.label "Turn on tab syncing to view a list of tabs from your other devices.">
<!ENTITY syncedTabs.context.openTab.label "Open This Tab">
<!ENTITY syncedTabs.context.openTab.accesskey "O">
<!ENTITY syncedTabs.context.bookmarkSingleTab.label "Bookmark This Tab…">
<!ENTITY syncedTabs.context.bookmarkSingleTab.accesskey "B">
<!ENTITY syncedTabs.context.refreshList.label "Refresh List">
<!ENTITY syncedTabs.context.refreshList.accesskey "R">
<!ENTITY syncBrand.shortName.label "Sync">
<!ENTITY syncSignIn.label "Sign In To &syncBrand.shortName.label;…">

Просмотреть файл

@ -9,6 +9,7 @@ browser.jar:
skin/classic/browser/sanitizeDialog.css
skin/classic/browser/aboutSessionRestore-window-icon.png
skin/classic/browser/aboutSyncTabs.css
* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css)
skin/classic/browser/actionicon-tab.png
* skin/classic/browser/browser.css
* skin/classic/browser/devedition.css

Просмотреть файл

@ -0,0 +1,58 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%include ../../shared/syncedtabs/sidebar.inc.css
/* These styles are intended to mimic XUL trees and the XUL search box. */
html {
border: 1px solid ThreeDShadow;
background-color: -moz-Field;
color: -moz-FieldText;
box-sizing: border-box;
}
.item {
-moz-padding-end: 0;
}
.item-title {
margin: 1px 0 0;
-moz-margin-end: 6px;
}
.search-box {
-moz-appearance: textfield;
cursor: text;
margin: 2px 4px;
border: 2px solid;
-moz-border-top-colors: ThreeDShadow ThreeDDarkShadow;
-moz-border-right-colors: ThreeDHighlight ThreeDLightShadow;
-moz-border-bottom-colors: ThreeDHighlight ThreeDLightShadow;
-moz-border-left-colors: ThreeDShadow ThreeDDarkShadow;
padding: 2px 2px 3px;
-moz-padding-start: 4px;
background-color: -moz-Field;
color: -moz-FieldText;
}
.textbox-search-clear {
background-image: url(moz-icon://stock/gtk-clear?size=menu);
background-repeat: no-repeat;
width: 16px;
height: 16px;
}
.textbox-search-icon {
background-image: url(moz-icon://stock/gtk-find?size=menu);
background-repeat: no-repeat;
width: 16px;
height: 16px;
}
.textbox-search-icon[searchbutton]:not([disabled]) ,
.textbox-search-clear:not([disabled]) {
cursor: pointer;
}

Просмотреть файл

@ -8,6 +8,9 @@ browser.jar:
skin/classic/browser/sanitizeDialog.css
skin/classic/browser/aboutSessionRestore-window-icon.png
skin/classic/browser/aboutSyncTabs.css
* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css)
skin/classic/browser/syncedtabs/arrow-open.svg (syncedtabs/arrow-open.svg)
skin/classic/browser/syncedtabs/arrow-closed.svg (syncedtabs/arrow-closed.svg)
skin/classic/browser/actionicon-tab.png
skin/classic/browser/actionicon-tab@2x.png
* skin/classic/browser/browser.css

Просмотреть файл

@ -8,7 +8,8 @@
#bookmarksPanel,
#history-panel,
#sidebar-search-container {
#sidebar-search-container,
#tabs-panel {
-moz-appearance: none !important;
background-color: transparent !important;
border-top: none !important;

Двоичные данные
browser/themes/osx/sync-desktopIcon.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 291 B

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичные данные
browser/themes/osx/sync-mobileIcon.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 352 B

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Просмотреть файл

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="15" height="18" viewBox="0 0 15 18">
<path id="arrow" d="M14.999,9.006 L-0.001,17.998 L-0.001,0.013 L14.999,9.006 z" fill="#8C8C8C" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 472 B

Просмотреть файл

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18" height="15" viewBox="0 0 18 15">
<path id="arrow-down" d="M8.994,14.999 L0.002,-0.001 L17.987,-0.001 L8.994,14.999 z" fill="#8C8C8C" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 477 B

Просмотреть файл

@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%include ../../shared/syncedtabs/sidebar.inc.css
/* These styles are intended to mimic XUL trees and the XUL search box. */
html {
}
.item {
color: -moz-DialogText;
}
.item.selected > .item-title-container {
color: HighlightText;
font-weight: bold;
}
.item.selected > .item-title-container {
background: linear-gradient(to bottom, rgba(156,172,204,1) 0%, rgba(116,135,172,1) 100%);
}
.item.selected:focus > .item-title-container {
background: linear-gradient(to bottom, rgba(95,144,209,1) 0%, rgba(39,90,173,1) 100%);
}
.item.client .item-twisty-container {
background-image: url(arrow-open.svg);
background-size: 9px 8px;
}
.item.client.closed .item-twisty-container {
background-image: url(arrow-closed.svg);
background-size: 7px 9px;
}
.sidebar-search-container {
border-bottom: 1px solid #bdbdbd;
}
.search-box {
-moz-appearance: searchfield;
padding: 1px;
font-size: 12px;
cursor: text;
margin: 4px 8px 10px;
border-width: 3px;
border-style: solid;
border-color: -moz-use-text-color;
border-image: none;
-moz-border-top-colors: transparent #888 #000;
-moz-border-right-colors: transparent #FFF #000;
-moz-border-bottom-colors: transparent #FFF #000;
-moz-border-left-colors: transparent #888 #000;
border-top-right-radius: 2px;
border-bottom-left-radius: 2px;
background-color: #FFF;
color: #000;
-moz-user-select: text;
text-shadow: none;
}
.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
width: 11px;
height: 11px;
background-image: url(chrome://global/skin/icons/searchfield-small-cancel.png);
background-repeat: no-repeat;
}
.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:active:hover {
background-position: 11px 0;
}
.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-icon {
display: none;
}
.search-box[focused="true"] {
-moz-border-top-colors: -moz-mac-focusring -moz-mac-focusring #000000;
-moz-border-right-colors: -moz-mac-focusring -moz-mac-focusring #000000;
-moz-border-bottom-colors: -moz-mac-focusring -moz-mac-focusring #000000;
-moz-border-left-colors: -moz-mac-focusring -moz-mac-focusring #000000;
}
.search-box.compact {
padding: 0px;
/* font size is in px because the XUL it was copied from uses px */
font-size: 11px;
}
.textbox-search-clear,
.textbox-search-icon {
margin-top: 1px;
}

Просмотреть файл

@ -81,6 +81,8 @@
skin/classic/browser/fxa/sync-illustration.svg (../shared/fxa/sync-illustration.svg)
skin/classic/browser/fxa/android.png (../shared/fxa/android.png)
skin/classic/browser/fxa/android@2x.png (../shared/fxa/android@2x.png)
skin/classic/browser/syncedtabs/twisty-closed.svg (../shared/syncedtabs/twisty-closed.svg)
skin/classic/browser/syncedtabs/twisty-open.svg (../shared/syncedtabs/twisty-open.svg)
skin/classic/browser/search-pref.png (../shared/search/search-pref.png)
skin/classic/browser/search-indicator.png (../shared/search/search-indicator.png)
skin/classic/browser/search-indicator@2x.png (../shared/search/search-indicator@2x.png)

Просмотреть файл

@ -0,0 +1,199 @@
% This Source Code Form is subject to the terms of the Mozilla Public
% License, v. 2.0. If a copy of the MPL was not distributed with this
% file, You can obtain one at http://mozilla.org/MPL/2.0/.
/* These styles are intended to mimic XUL trees and the XUL search box. */
:root, body {
overflow-x: hidden;
}
body {
margin: 0;
font: message-box;
color: #333333;
-moz-user-select: none;
overflow: hidden;
}
.emptyListInfo {
cursor: default;
padding: 3em 1em;
text-align: center;
}
.list,
.item-tabs-list {
display: flex;
flex-flow: column;
flex-grow: 1;
}
.item.client {
opacity: 1;
max-height: unset;
display: unset;
}
.item.client.closed .item-tabs-list {
display: none;
}
.item {
display: inline-block;
opacity: 1;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
outline: none;
color: -moz-FieldText;
}
.item.selected > .item-title-container {
background-color: -moz-cellhighlight;
color: -moz-cellhighlighttext;
font-weight: bold;
}
.item.selected:focus > .item-title-container {
background-color: Highlight;
color: HighlightText;
}
.client .item.tab > .item-title-container {
padding-inline-start: 35px;
}
.item.tab > .item-title-container {
padding-inline-start: 20px;
}
.item-icon-container {
min-width: 16px;
max-width: 16px;
min-height: 16px;
max-height: 16px;
margin-right: 5px;
background-size: 16px 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.item-twisty-container {
min-width: 16px;
max-width: 16px;
min-height: 16px;
max-height: 16px;
margin-right: 5px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.item-icon-container {
margin-left: 5px;
}
.item-title-container {
display: flex;
flex-flow: row;
overflow: hidden;
flex-grow: 1;
padding: 1px 0px 1px 0px;
}
.item-title {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
line-height: 1.3;
}
.item[hidden] {
opacity: 0;
max-height: 0;
transition: opacity 150ms ease-in-out, max-height 150ms ease-in-out 150ms;
}
.item.empty .item-title-container {
color: #aeaeae;
}
.client .item.empty > .item-title-container {
padding-inline-start: 35px;
}
.text-input-box {
display: flex;
flex-flow: row nowrap;
}
.textbox-input-box {
display: flex;
flex-direction: row;
}
.tabsFilter {
flex: 1;
}
.sync-state > p {
padding-inline-end: 10px;
padding-inline-start: 10px;
color: #888;
}
.text-link {
color: rgb(0, 149, 221);
cursor: pointer;
}
.text-link:hover {
text-decoration: underline;
}
.text-link,
.text-link:focus {
margin: 0px;
padding: 0px;
border: 0px;
}
.deck .sync-state {
display: none;
opacity: 0;
transition: opacity 1.5s;
border-top: 1px solid #bdbdbd;
}
.deck .sync-state.tabs-container {
border-top: 0px;
}
.deck .sync-state.selected {
display: unset;
opacity: 100;
}
.item.client .item-twisty-container {
background-image: url(twisty-open.svg);
}
.item.client.closed .item-twisty-container {
background-image: url(twisty-closed.svg);
}
.textbox-search-clear:not([disabled]) {
cursor: default;
}
.textbox-search-icons .textbox-search-clear,
.filtered .textbox-search-icons .textbox-search-icon {
display: none;
}
.filtered .textbox-search-icons .textbox-search-clear {
display: block;
}

Просмотреть файл

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="11"
height="11">
<defs>
<linearGradient
id="linearGradient3792">
<stop
style="stop-color:#c3baaa;stop-opacity:1"
offset="0" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
id="linearGradient2996"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,0.88461522,0.8846152)" />
</defs>
<rect
width="8"
height="8"
rx="1"
ry="1"
x="1.5"
y="1.5"
style="fill:url(#linearGradient2996);fill-opacity:1;stroke:#7898b5;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="M 5,3 5,5 3,5 3,6 5,6 5,8 6,8 6,6 8,6 8,5 6,5 6,3 5,3 z"
style="fill:#000000;fill-opacity:1;stroke:none" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.9 KiB

Просмотреть файл

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="11"
height="11">
<defs>
<linearGradient
id="linearGradient3792">
<stop
style="stop-color:#c3baaa;stop-opacity:1"
offset="0" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,-0.11538478,1.8846152)" />
<linearGradient
x1="6.0530181"
y1="7.092885"
x2="2.8882971"
y2="1.7999334"
id="linearGradient2996"
xlink:href="#linearGradient3792"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0256411,0,0,1.0256411,0.88461522,0.8846152)" />
</defs>
<rect
width="8"
height="8"
rx="1"
ry="1"
x="1.5"
y="1.5"
style="fill:url(#linearGradient2996);fill-opacity:1;stroke:#7898b5;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<rect
width="5"
height="1"
x="3"
y="5"
style="fill:#000000;fill-opacity:1;stroke:none" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.9 KiB

Просмотреть файл

@ -8,6 +8,7 @@ browser.jar:
skin/classic/browser/sanitizeDialog.css
skin/classic/browser/aboutSessionRestore-window-icon.png
skin/classic/browser/aboutSyncTabs.css
* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css)
skin/classic/browser/actionicon-tab.png
skin/classic/browser/actionicon-tab@2x.png
skin/classic/browser/actionicon-tab-XPVista7.png

Просмотреть файл

@ -40,7 +40,8 @@
@media (-moz-os-version: windows-vista),
(-moz-os-version: windows-win7) {
#bookmarksPanel,
#history-panel {
#history-panel,
#tabs-panel {
background-color: #EEF3FA;
}
}

Просмотреть файл

@ -0,0 +1,103 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%include ../../shared/syncedtabs/sidebar.inc.css
/* These styles are intended to mimic XUL trees and the XUL search box. */
html {
background-color: #EEF3FA;
}
.item {
-moz-padding-end: 0;
}
.item-title {
margin: 1px 0 0;
}
.item-title {
-moz-margin-end: 6px;
}
.search-box {
-moz-appearance: textfield;
cursor: text;
margin: 2px 4px;
padding: 2px 2px 3px;
-moz-padding-start: 4px;
color: -moz-FieldText;
}
.textbox-search-icon {
width: 16px;
height: 16px;
background-image: url(chrome://global/skin/icons/Search-glass.png);
background-repeat: no-repeat;
display: block;
}
.textbox-search-icon:-moz-locale-dir(rtl) {
transform: scaleX(-1);
}
.textbox-search-icon[searchbutton]:not([disabled]) {
cursor: pointer;
}
.textbox-search-clear {
width: 16px;
height: 16px;
background-image: url(chrome://global/skin/icons/Search-close.png);
background-repeat: no-repeat;
}
.textbox-search-clear:not([disabled]) {
cursor: default;
}
.textbox-search-icon:not([disabled]) {
cursor: text;
}
.textbox-search-clear:not([disabled]):hover ,
.textbox-search-icon:not([disabled]):hover {
background-position: -16px 0;
}
.textbox-search-clear:not([disabled]):hover:active ,
.textbox-search-icon:not([disabled]):hover:active {
background-position: -32px 0;
}
.client .item.tab > .item-title-container {
padding-inline-start: 26px;
}
.item.tab > .item-title-container {
padding-inline-start: 14px;
}
.item-icon-container {
min-width: 16px;
max-width: 16px;
min-height: 16px;
max-height: 16px;
margin-right: 5px;
background-size: 16px 16px;
background-repeat: no-repeat;
background-position: center;
}
.item-twisty-container {
min-width: 12px;
max-width: 12px;
min-height: 12px;
max-height: 12px;
margin: 0 1px;
background-size: 16px 16px;
background-repeat: no-repeat;
background-position: center;
}

Просмотреть файл

@ -13,6 +13,7 @@ TESTING_JS_MODULES += [
'Assert.jsm',
'CoverageUtils.jsm',
'MockRegistrar.jsm',
'sinon-1.16.1.js',
'StructuredLog.jsm',
'TestUtils.jsm',
]

Разница между файлами не показана из-за своего большого размера Загрузить разницу