merge mozilla-inbound to mozilla-central a=merge
@ -39,7 +39,6 @@ parser/**
|
||||
probes/**
|
||||
python/**
|
||||
rdf/**
|
||||
security/**
|
||||
services/**
|
||||
startupcache/**
|
||||
testing/**
|
||||
|
@ -567,7 +567,7 @@ finalizeCB(GObject *aObj)
|
||||
{
|
||||
if (!IS_MAI_OBJECT(aObj))
|
||||
return;
|
||||
NS_ASSERTION(MAI_ATK_OBJECT(aObj)->accWrap.Bits() == 0, "AccWrap NOT null");
|
||||
NS_ASSERTION(MAI_ATK_OBJECT(aObj)->accWrap.IsNull(), "AccWrap NOT null");
|
||||
|
||||
// call parent finalize function
|
||||
// finalize of GObjectClass will unref the accessible parent if has
|
||||
@ -650,22 +650,17 @@ getRoleCB(AtkObject *aAtkObj)
|
||||
if (aAtkObj->role != ATK_ROLE_INVALID)
|
||||
return aAtkObj->role;
|
||||
|
||||
AccessibleWrap* accWrap = GetAccessibleWrap(aAtkObj);
|
||||
a11y::role role;
|
||||
if (!accWrap) {
|
||||
ProxyAccessible* proxy = GetProxy(aAtkObj);
|
||||
if (!proxy)
|
||||
return ATK_ROLE_INVALID;
|
||||
AccessibleOrProxy acc = GetInternalObj(aAtkObj);
|
||||
if (acc.IsNull()) {
|
||||
return ATK_ROLE_INVALID;
|
||||
}
|
||||
|
||||
role = proxy->Role();
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
if (AccessibleWrap* accWrap = GetAccessibleWrap(aAtkObj)) {
|
||||
NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(accWrap),
|
||||
"Does not support Text interface when it should");
|
||||
#endif
|
||||
|
||||
role = accWrap->Role();
|
||||
}
|
||||
#endif
|
||||
|
||||
#define ROLE(geckoRole, stringRole, atkRole, macRole, \
|
||||
msaaRole, ia2Role, nameRule) \
|
||||
@ -673,7 +668,7 @@ getRoleCB(AtkObject *aAtkObj)
|
||||
aAtkObj->role = atkRole; \
|
||||
break;
|
||||
|
||||
switch (role) {
|
||||
switch (acc.Role()) {
|
||||
#include "RoleMap.h"
|
||||
default:
|
||||
MOZ_CRASH("Unknown role.");
|
||||
@ -1088,11 +1083,16 @@ GetAccessibleWrap(AtkObject* aAtkObj)
|
||||
ProxyAccessible*
|
||||
GetProxy(AtkObject* aObj)
|
||||
{
|
||||
if (!aObj || !IS_MAI_OBJECT(aObj) ||
|
||||
!MAI_ATK_OBJECT(aObj)->accWrap.IsProxy())
|
||||
return GetInternalObj(aObj).AsProxy();
|
||||
}
|
||||
|
||||
AccessibleOrProxy
|
||||
GetInternalObj(AtkObject* aObj)
|
||||
{
|
||||
if (!aObj || !IS_MAI_OBJECT(aObj))
|
||||
return nullptr;
|
||||
|
||||
return MAI_ATK_OBJECT(aObj)->accWrap.AsProxy();
|
||||
return MAI_ATK_OBJECT(aObj)->accWrap;
|
||||
}
|
||||
|
||||
AtkObject*
|
||||
|
@ -67,6 +67,7 @@ typedef struct _MaiAtkSocketClass
|
||||
|
||||
mozilla::a11y::AccessibleWrap* GetAccessibleWrap(AtkObject* aAtkObj);
|
||||
mozilla::a11y::ProxyAccessible* GetProxy(AtkObject* aAtkObj);
|
||||
mozilla::a11y::AccessibleOrProxy GetInternalObj(AtkObject* aObj);
|
||||
AtkObject* GetWrapperFor(mozilla::a11y::ProxyAccessible* aProxy);
|
||||
|
||||
extern int atkMajorVersion, atkMinorVersion;
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "mozilla/a11y/Accessible.h"
|
||||
#include "mozilla/a11y/ProxyAccessible.h"
|
||||
#include "mozilla/a11y/Role.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
@ -48,6 +49,63 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsNull() const { return mBits == 0; }
|
||||
|
||||
uint32_t ChildCount() const
|
||||
{
|
||||
if (IsProxy()) {
|
||||
return AsProxy()->ChildrenCount();
|
||||
}
|
||||
|
||||
return AsAccessible()->ChildCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the child object either an accessible or a proxied accessible at
|
||||
* the given index.
|
||||
*/
|
||||
AccessibleOrProxy ChildAt(uint32_t aIdx)
|
||||
{
|
||||
if (IsProxy()) {
|
||||
return AsProxy()->ChildAt(aIdx);
|
||||
}
|
||||
|
||||
return AsAccessible()->GetChildAt(aIdx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first child object.
|
||||
*/
|
||||
AccessibleOrProxy FirstChild()
|
||||
{
|
||||
if (IsProxy()) {
|
||||
return AsProxy()->FirstChild();
|
||||
}
|
||||
|
||||
return AsAccessible()->FirstChild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first child object.
|
||||
*/
|
||||
AccessibleOrProxy LastChild()
|
||||
{
|
||||
if (IsProxy()) {
|
||||
return AsProxy()->LastChild();
|
||||
}
|
||||
|
||||
return AsAccessible()->LastChild();
|
||||
}
|
||||
|
||||
role Role() const
|
||||
{
|
||||
if (IsProxy()) {
|
||||
return AsProxy()->Role();
|
||||
}
|
||||
|
||||
return AsAccessible()->Role();
|
||||
}
|
||||
|
||||
// XXX these are implementation details that ideally would not be exposed.
|
||||
uintptr_t Bits() const { return mBits; }
|
||||
void SetBits(uintptr_t aBits) { mBits = aBits; }
|
||||
|
@ -110,7 +110,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=410052
|
||||
[ 0, 1, 2, -1, -1],
|
||||
[-1, -1, -1, -1, -1],
|
||||
[ 3, 4, 5, -1, -1],
|
||||
[ 6, 7, 7, 7, 7],
|
||||
[ 6, 7, -1, -1, -1],
|
||||
[ 6, 8, 9, -1, -1],
|
||||
[ 6, 10, 9, 11, 12]
|
||||
];
|
||||
|
@ -65,10 +65,10 @@ xpcAccessible::GetFirstChild(nsIAccessible** aFirstChild)
|
||||
NS_ENSURE_ARG_POINTER(aFirstChild);
|
||||
*aFirstChild = nullptr;
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
NS_IF_ADDREF(*aFirstChild = ToXPC(Intl()->FirstChild()));
|
||||
NS_IF_ADDREF(*aFirstChild = ToXPC(IntlGeneric().FirstChild()));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -78,10 +78,10 @@ xpcAccessible::GetLastChild(nsIAccessible** aLastChild)
|
||||
NS_ENSURE_ARG_POINTER(aLastChild);
|
||||
*aLastChild = nullptr;
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
NS_IF_ADDREF(*aLastChild = ToXPC(Intl()->LastChild()));
|
||||
NS_IF_ADDREF(*aLastChild = ToXPC(IntlGeneric().LastChild()));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -90,10 +90,10 @@ xpcAccessible::GetChildCount(int32_t* aChildCount)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aChildCount);
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
*aChildCount = Intl()->ChildCount();
|
||||
*aChildCount = IntlGeneric().ChildCount();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -103,16 +103,16 @@ xpcAccessible::GetChildAt(int32_t aChildIndex, nsIAccessible** aChild)
|
||||
NS_ENSURE_ARG_POINTER(aChild);
|
||||
*aChild = nullptr;
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
// If child index is negative, then return last child.
|
||||
// XXX: do we really need this?
|
||||
if (aChildIndex < 0)
|
||||
aChildIndex = Intl()->ChildCount() - 1;
|
||||
aChildIndex = IntlGeneric().ChildCount() - 1;
|
||||
|
||||
Accessible* child = Intl()->GetChildAt(aChildIndex);
|
||||
if (!child)
|
||||
AccessibleOrProxy child = IntlGeneric().ChildAt(aChildIndex);
|
||||
if (child.IsNull())
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
|
||||
NS_ADDREF(*aChild = ToXPC(child));
|
||||
@ -125,7 +125,7 @@ xpcAccessible::GetChildren(nsIArray** aChildren)
|
||||
NS_ENSURE_ARG_POINTER(aChildren);
|
||||
*aChildren = nullptr;
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
nsresult rv = NS_OK;
|
||||
@ -133,13 +133,13 @@ xpcAccessible::GetChildren(nsIArray** aChildren)
|
||||
do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
uint32_t childCount = Intl()->ChildCount();
|
||||
uint32_t childCount = IntlGeneric().ChildCount();
|
||||
for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
|
||||
Accessible* child = Intl()->GetChildAt(childIdx);
|
||||
AccessibleOrProxy child = IntlGeneric().ChildAt(childIdx);
|
||||
children->AppendElement(static_cast<nsIAccessible*>(ToXPC(child)), false);
|
||||
}
|
||||
|
||||
NS_ADDREF(*aChildren = children);
|
||||
children.forget(aChildren);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@ -204,10 +204,10 @@ xpcAccessible::GetRole(uint32_t* aRole)
|
||||
NS_ENSURE_ARG_POINTER(aRole);
|
||||
*aRole = nsIAccessibleRole::ROLE_NOTHING;
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
*aRole = Intl()->Role();
|
||||
*aRole = IntlGeneric().Role();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ namespace mozilla {
|
||||
namespace a11y {
|
||||
|
||||
class Accessible;
|
||||
class AccessibleOrProxy;
|
||||
|
||||
/**
|
||||
* XPCOM nsIAccessible interface implementation, used by xpcAccessibleGeneric
|
||||
@ -92,6 +93,7 @@ protected:
|
||||
|
||||
private:
|
||||
Accessible* Intl();
|
||||
AccessibleOrProxy IntlGeneric();
|
||||
|
||||
xpcAccessible(const xpcAccessible&) = delete;
|
||||
xpcAccessible& operator =(const xpcAccessible&) = delete;
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "DocAccessible-inl.h"
|
||||
#include "nsIDOMDocument.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::a11y;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@ -227,3 +228,18 @@ xpcAccessibleDocument::Shutdown()
|
||||
}
|
||||
xpcAccessibleGeneric::Shutdown();
|
||||
}
|
||||
|
||||
xpcAccessibleGeneric*
|
||||
a11y::ToXPC(AccessibleOrProxy aAcc)
|
||||
{
|
||||
if (aAcc.IsNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aAcc.IsAccessible()) {
|
||||
return ToXPC(aAcc.AsAccessible());
|
||||
}
|
||||
|
||||
xpcAccessibleDocument* doc = ToXPCDocument(aAcc.AsProxy()->Document());
|
||||
return doc->GetXPCAccessible(aAcc.AsProxy());
|
||||
}
|
||||
|
@ -118,6 +118,8 @@ ToXPC(Accessible* aAccessible)
|
||||
return xpcDoc ? xpcDoc->GetAccessible(aAccessible) : nullptr;
|
||||
}
|
||||
|
||||
xpcAccessibleGeneric* ToXPC(AccessibleOrProxy aAcc);
|
||||
|
||||
inline xpcAccessibleHyperText*
|
||||
ToXPCText(HyperTextAccessible* aAccessible)
|
||||
{
|
||||
@ -135,6 +137,12 @@ ToXPCDocument(DocAccessible* aAccessible)
|
||||
return GetAccService()->GetXPCDocument(aAccessible);
|
||||
}
|
||||
|
||||
inline xpcAccessibleDocument*
|
||||
ToXPCDocument(DocAccessibleParent* aAccessible)
|
||||
{
|
||||
return GetAccService()->GetXPCDocument(aAccessible);
|
||||
}
|
||||
|
||||
} // namespace a11y
|
||||
} // namespace mozilla
|
||||
|
||||
|
@ -91,6 +91,12 @@ xpcAccessible::Intl()
|
||||
return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible();
|
||||
}
|
||||
|
||||
inline AccessibleOrProxy
|
||||
xpcAccessible::IntlGeneric()
|
||||
{
|
||||
return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
|
||||
}
|
||||
|
||||
inline Accessible*
|
||||
xpcAccessibleHyperLink::Intl()
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [
|
||||
'menu_socialSidebar',
|
||||
'menu_historySidebar',
|
||||
'menu_bookmarksSidebar',
|
||||
'menu_tabsSidebar',
|
||||
];
|
||||
|
||||
function isSidebarShowing(window) {
|
||||
|
@ -33,16 +33,6 @@ var gEMEHandler = {
|
||||
return "<label class='text-link' href='" + baseURL + "drm-content'>" +
|
||||
text + "</label>";
|
||||
},
|
||||
onDontAskAgain: function(menuPopupItem) {
|
||||
let button = menuPopupItem.parentNode.anchorNode;
|
||||
let bar = button.parentNode;
|
||||
Services.prefs.setBoolPref("browser.eme.ui." + bar.value + ".disabled", true);
|
||||
bar.close();
|
||||
},
|
||||
onNotNow: function(menuPopupItem) {
|
||||
let button = menuPopupItem.parentNode.anchorNode;
|
||||
button.parentNode.close();
|
||||
},
|
||||
receiveMessage: function({target: browser, data: data}) {
|
||||
let parsedData;
|
||||
try {
|
||||
@ -105,23 +95,14 @@ var gEMEHandler = {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user turned these off, bail out:
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("browser.eme.ui." + notificationId + ".disabled")) {
|
||||
return;
|
||||
}
|
||||
} catch (ex) { /* Don't care if the pref doesn't exist */ }
|
||||
|
||||
let msgPrefix = "emeNotifications." + notificationId + ".";
|
||||
let msgId = msgPrefix + "message";
|
||||
|
||||
// Specialcase Adobe's CDM on unsupported platforms to be more informative:
|
||||
// Special-case Adobe's CDM message on unsupported platforms to be more informative:
|
||||
if (notificationId == "drmContentCDMNotSupported" &&
|
||||
keySystem.startsWith("com.adobe")) {
|
||||
let os = Services.appinfo.OS.toLowerCase();
|
||||
if (os.startsWith("win") && Services.appinfo.XPCOMABI.startsWith("x86_64")) {
|
||||
msgId = msgPrefix + "64bit.message";
|
||||
} else if (os.startsWith("linux") || os.startsWith("darwin")) {
|
||||
if (os.startsWith("linux") || os.startsWith("darwin")) {
|
||||
msgId = msgPrefix + "unsupportedOS.message";
|
||||
labelParams.splice(1, 0, os.startsWith("linux") ? "Linux" : "Mac OS X");
|
||||
}
|
||||
@ -140,13 +121,6 @@ var gEMEHandler = {
|
||||
accessKey: gNavigatorBundle.getString(btnAccessKeyId),
|
||||
callback: callback
|
||||
});
|
||||
|
||||
let optionsId = "emeNotifications.optionsButton";
|
||||
buttons.push({
|
||||
label: gNavigatorBundle.getString(optionsId + ".label"),
|
||||
accessKey: gNavigatorBundle.getString(optionsId + ".accesskey"),
|
||||
popup: "emeNotificationsPopup"
|
||||
});
|
||||
}
|
||||
|
||||
let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black";
|
||||
@ -196,7 +170,6 @@ var gEMEHandler = {
|
||||
document.getElementById(anchorId).removeAttribute("firstplay");
|
||||
}
|
||||
|
||||
|
||||
let mainAction = {
|
||||
label: gNavigatorBundle.getString(btnLabelId),
|
||||
accessKey: gNavigatorBundle.getString(btnAccessKeyId),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -464,15 +464,17 @@
|
||||
<tooltip id="dynamic-shortcut-tooltip"
|
||||
onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/>
|
||||
|
||||
<menupopup id="emeNotificationsPopup">
|
||||
<menuitem id="emeNotificationsNotNow"
|
||||
label="&emeNotificationsNotNow.label;"
|
||||
acceskey="&emeNotificationsNotNow.accesskey;"
|
||||
oncommand="gEMEHandler.onNotNow(this);"/>
|
||||
<menuitem id="emeNotificationsDontAskAgain"
|
||||
label="&emeNotificationsDontAskAgain.label;"
|
||||
acceskey="&emeNotificationsDontAskAgain.accesskey;"
|
||||
oncommand="gEMEHandler.onDontAskAgain(this);"/>
|
||||
<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>
|
||||
|
||||
|
@ -47,6 +47,3 @@ support-files =
|
||||
[browser_newtab_bug1178586.js]
|
||||
[browser_newtab_bug1194895.js]
|
||||
[browser_newtab_1188015.js]
|
||||
[browser_newtab_external_resource.js]
|
||||
support-files =
|
||||
external_newtab.html
|
||||
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Description of the Tests for
|
||||
* - Bug 1204983 - Allow about: pages to load remote content
|
||||
*
|
||||
* We perform two tests:
|
||||
* (1) We load a new tab (about:newtab) using the default url and make sure that URL
|
||||
* of the doucment matches about:newtab and the principal is the systemPrincipal.
|
||||
* (2) We load a new tab (about:newtab) and make sure that document.location as well
|
||||
* as the nodePrincipal match the URL in the URL bar.
|
||||
*/
|
||||
|
||||
/* globals Cc, Ci, ok, is, content, TestRunner, addNewTabPageTab, gWindow, Services, info */
|
||||
/* exported runTests */
|
||||
|
||||
"use strict";
|
||||
|
||||
var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
|
||||
.getService(Ci.nsIAboutNewTabService);
|
||||
|
||||
const ABOUT_NEWTAB_URI = "about:newtab";
|
||||
const PREF_URI = "http://example.com/browser/browser/base/content/test/newtab/external_newtab.html";
|
||||
const DEFAULT_URI = aboutNewTabService.newTabURL;
|
||||
|
||||
function* loadNewPageAndVerify(browser, uri) {
|
||||
let browserLoadedPromise = BrowserTestUtils.waitForEvent(browser, "load", true);
|
||||
browser.loadURI("about:newtab");
|
||||
yield browserLoadedPromise;
|
||||
|
||||
yield ContentTask.spawn(gBrowser.selectedBrowser, { uri: uri }, function* (args) {
|
||||
let uri = args.uri;
|
||||
|
||||
is(String(content.document.location), uri, "document.location should match " + uri);
|
||||
is(content.document.documentURI, uri, "document.documentURI should match " + uri);
|
||||
|
||||
if (uri == "about:newtab") {
|
||||
is(content.document.nodePrincipal,
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
"nodePrincipal should match systemPrincipal");
|
||||
}
|
||||
else {
|
||||
is(content.document.nodePrincipal.URI.spec, uri,
|
||||
"nodePrincipal should match " + uri);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
// test the default behavior
|
||||
yield* addNewTabPageTab();
|
||||
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
|
||||
ok(!aboutNewTabService.overridden,
|
||||
"sanity check: default URL for about:newtab should not be overriden");
|
||||
|
||||
yield* loadNewPageAndVerify(browser, ABOUT_NEWTAB_URI);
|
||||
|
||||
// set the pref for about:newtab to point to an exteranl resource
|
||||
aboutNewTabService.newTabURL = PREF_URI;
|
||||
ok(aboutNewTabService.overridden,
|
||||
"sanity check: default URL for about:newtab should be overriden");
|
||||
is(aboutNewTabService.newTabURL, PREF_URI,
|
||||
"sanity check: default URL for about:newtab should return the new URL");
|
||||
|
||||
yield* loadNewPageAndVerify(browser, PREF_URI);
|
||||
|
||||
// reset to about:newtab and perform sanity check
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
is(aboutNewTabService.newTabURL, DEFAULT_URI,
|
||||
"sanity check: resetting the URL to about:newtab should return about:newtab");
|
||||
|
||||
// remove the tab and move on
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1204983 -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Testpage for bug 1204983</title>
|
||||
</head>
|
||||
<body>
|
||||
Just a testpage for Bug 1204983<br/>
|
||||
</body>
|
||||
</html>
|
@ -23,7 +23,7 @@ this.__defineGetter__("BROWSER_NEW_TAB_URL", () => {
|
||||
!aboutNewTabService.overridden) {
|
||||
return "about:privatebrowsing";
|
||||
}
|
||||
return "about:newtab";
|
||||
return aboutNewTabService.newTabURL;
|
||||
});
|
||||
|
||||
var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
|
||||
|
@ -162,7 +162,8 @@ AboutRedirector::NewChannel(nsIURI* aURI,
|
||||
// let the aboutNewTabService decide where to redirect
|
||||
nsCOMPtr<nsIAboutNewTabService> aboutNewTabService =
|
||||
do_GetService("@mozilla.org/browser/aboutnewtab-service;1", &rv);
|
||||
rv = aboutNewTabService->GetNewTabURL(url);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = aboutNewTabService->GetDefaultURL(url);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
// fall back to the specified url in the map
|
||||
|
@ -101,7 +101,6 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "remote-newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
|
@ -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"
|
||||
|
@ -51,7 +51,7 @@ add_task(function* () {
|
||||
windowId: activeWindow,
|
||||
active: true,
|
||||
pinned: false,
|
||||
url: "chrome://browser/content/newtab/newTab.xhtml",
|
||||
url: "about:newtab",
|
||||
};
|
||||
|
||||
let tests = [
|
||||
@ -65,7 +65,7 @@ add_task(function* () {
|
||||
},
|
||||
{
|
||||
create: {},
|
||||
result: { url: "chrome://browser/content/newtab/newTab.xhtml" },
|
||||
result: { url: "about:newtab" },
|
||||
},
|
||||
{
|
||||
create: { active: false },
|
||||
|
@ -21,6 +21,7 @@ DIRS += [
|
||||
'sessionstore',
|
||||
'shell',
|
||||
'selfsupport',
|
||||
'syncedtabs',
|
||||
'uitour',
|
||||
'translation',
|
||||
]
|
||||
|
@ -1,2 +1,2 @@
|
||||
component {cef25b06-0ef6-4c50-a243-e69f943ef23d} aboutNewTabService.js
|
||||
contract @mozilla.org/browser/aboutnewtab-service;1 {cef25b06-0ef6-4c50-a243-e69f943ef23d}
|
||||
component {dfcd2adc-7867-4d3a-ba70-17501f208142} aboutNewTabService.js
|
||||
contract @mozilla.org/browser/aboutnewtab-service;1 {dfcd2adc-7867-4d3a-ba70-17501f208142}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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/. */
|
||||
|
||||
/* globals XPCOMUtils, Deprecated, aboutNewTabService*/
|
||||
/* globals XPCOMUtils, aboutNewTabService*/
|
||||
/* exported NewTabURL */
|
||||
|
||||
"use strict";
|
||||
|
@ -26,6 +26,8 @@ const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";
|
||||
const REMOTE_NEWTAB_URL = "https://newtab.cdn.mozilla.net/" +
|
||||
"v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
|
||||
|
||||
const ABOUT_URL = "about:newtab";
|
||||
|
||||
// Pref that tells if remote newtab is enabled
|
||||
const PREF_REMOTE_ENABLED = "browser.newtabpage.remote";
|
||||
|
||||
@ -46,25 +48,65 @@ function AboutNewTabService() {
|
||||
this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED));
|
||||
}
|
||||
|
||||
/*
|
||||
* A service that allows for the overriding, at runtime, of the newtab page's url.
|
||||
* Additionally, the service manages pref state between a remote and local newtab page.
|
||||
*
|
||||
* There is tight coupling with browser/about/AboutRedirector.cpp.
|
||||
*
|
||||
* 1. Browser chrome access:
|
||||
*
|
||||
* When the user issues a command to open a new tab page, usually clicking a button
|
||||
* in the browser chrome or using shortcut keys, the browser chrome code invokes the
|
||||
* service to obtain the newtab URL. It then loads that URL in a new tab.
|
||||
*
|
||||
* When not overridden, the default URL emitted by the service is "about:newtab".
|
||||
* When overridden, it returns the overriden URL.
|
||||
*
|
||||
* 2. Redirector Access:
|
||||
*
|
||||
* When the URL loaded is about:newtab, the default behavior, or when entered in the
|
||||
* URL bar, the redirector is hit. The service is then called to return either of
|
||||
* two URLs, a chrome or remote one, based on the browser.newtabpage.remote pref.
|
||||
*
|
||||
* NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
|
||||
*
|
||||
* Access patterns:
|
||||
*
|
||||
* The behavior is different when accessing the service via browser chrome or via redirector
|
||||
* largely to maintain compatibility with expectations of add-on developers.
|
||||
*
|
||||
* Loading a chrome resource, or an about: URL in the redirector with either the
|
||||
* LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
|
||||
* to the redirector from browser chrome is avoided.
|
||||
*/
|
||||
AboutNewTabService.prototype = {
|
||||
|
||||
_newTabURL: LOCAL_NEWTAB_URL,
|
||||
_newTabURL: ABOUT_URL,
|
||||
_remoteEnabled: false,
|
||||
_remoteURL: null,
|
||||
_overridden: false,
|
||||
|
||||
classID: Components.ID("{cef25b06-0ef6-4c50-a243-e69f943ef23d}"),
|
||||
classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
|
||||
_xpcom_categories: [{
|
||||
service: true
|
||||
}],
|
||||
|
||||
_handleToggleEvent(prefName, stateEnabled, forceState) { //jshint unused:false
|
||||
this.toggleRemote(stateEnabled, forceState);
|
||||
if (this.toggleRemote(stateEnabled, forceState)) {
|
||||
Services.obs.notifyObservers(null, "newtab-url-changed", ABOUT_URL);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* React to changes to the remote newtab pref. Only act
|
||||
* if there is a change of state and if not overridden.
|
||||
* React to changes to the remote newtab pref.
|
||||
*
|
||||
* If browser.newtabpage.remote is true, this will change the default URL to the
|
||||
* remote newtab page URL. If browser.newtabpage.remote is false, the default URL
|
||||
* will be a local chrome URL.
|
||||
*
|
||||
* This will only act if there is a change of state and if not overridden.
|
||||
*
|
||||
* @returns {Boolean} Returns if there has been a state change
|
||||
*
|
||||
@ -79,7 +121,7 @@ AboutNewTabService.prototype = {
|
||||
}
|
||||
|
||||
if (stateEnabled) {
|
||||
this._newTabURL = this.generateRemoteURL();
|
||||
this._remoteURL = this.generateRemoteURL();
|
||||
NewTabPrefsProvider.prefs.on(
|
||||
PREF_SELECTED_LOCALE,
|
||||
this._updateRemoteMaybe.bind(this));
|
||||
@ -88,11 +130,11 @@ AboutNewTabService.prototype = {
|
||||
this._updateRemoteMaybe.bind(this));
|
||||
this._remoteEnabled = true;
|
||||
} else {
|
||||
this._newTabURL = LOCAL_NEWTAB_URL;
|
||||
NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateRemoteMaybe);
|
||||
NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateRemoteMaybe);
|
||||
this._remoteEnabled = false;
|
||||
}
|
||||
this._newTabURL = ABOUT_URL;
|
||||
return true;
|
||||
},
|
||||
|
||||
@ -108,6 +150,21 @@ AboutNewTabService.prototype = {
|
||||
return url;
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns the default URL.
|
||||
*
|
||||
* This URL only depends on the browser.newtabpage.remote pref. Overriding
|
||||
* the newtab page has no effect on the result of this function.
|
||||
*
|
||||
* @returns {String} the default newtab URL, remote or local depending on browser.newtabpage.remote
|
||||
*/
|
||||
get defaultURL() {
|
||||
if (this._remoteEnabled) {
|
||||
return this._remoteURL;
|
||||
}
|
||||
return LOCAL_NEWTAB_URL;
|
||||
},
|
||||
|
||||
/*
|
||||
* Updates the remote location when the page is not overriden.
|
||||
*
|
||||
@ -119,17 +176,17 @@ AboutNewTabService.prototype = {
|
||||
}
|
||||
|
||||
let url = this.generateRemoteURL();
|
||||
if (url !== this._newTabURL) {
|
||||
this._newTabURL = url;
|
||||
if (url !== this._remoteURL) {
|
||||
this._remoteURL = url;
|
||||
Services.obs.notifyObservers(null, "newtab-url-changed",
|
||||
this._newTabURL);
|
||||
this._remoteURL);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the release name from an Update Channel name
|
||||
*
|
||||
* @return {String} a release name based on the update channel. Defaults to nightly
|
||||
* @returns {String} a release name based on the update channel. Defaults to nightly
|
||||
*/
|
||||
releaseFromUpdateChannel(channelName) {
|
||||
return VALID_CHANNELS.has(channelName) ? channelName : "nightly";
|
||||
@ -148,10 +205,13 @@ AboutNewTabService.prototype = {
|
||||
},
|
||||
|
||||
set newTabURL(aNewTabURL) {
|
||||
if (aNewTabURL === "about:newtab") {
|
||||
aNewTabURL = aNewTabURL.trim();
|
||||
if (aNewTabURL === ABOUT_URL) {
|
||||
// avoid infinite redirects in case one sets the URL to about:newtab
|
||||
this.resetNewTabURL();
|
||||
return;
|
||||
} else if (aNewTabURL === "") {
|
||||
aNewTabURL = "about:blank";
|
||||
}
|
||||
let remoteURL = this.generateRemoteURL();
|
||||
let prefRemoteEnabled = Services.prefs.getBoolPref(PREF_REMOTE_ENABLED);
|
||||
@ -182,6 +242,7 @@ AboutNewTabService.prototype = {
|
||||
|
||||
resetNewTabURL() {
|
||||
this._overridden = false;
|
||||
this._newTabURL = ABOUT_URL;
|
||||
this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED), true);
|
||||
Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
* than the one specified within AboutRedirector.cpp
|
||||
*/
|
||||
|
||||
[scriptable, uuid(cef25b06-0ef6-4c50-a243-e69f943ef23d)]
|
||||
[scriptable, uuid(dfcd2adc-7867-4d3a-ba70-17501f208142)]
|
||||
interface nsIAboutNewTabService : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -18,6 +18,11 @@ interface nsIAboutNewTabService : nsISupports
|
||||
*/
|
||||
attribute ACString newTabURL;
|
||||
|
||||
/**
|
||||
* Returns the default URL (remote or local depending on pref)
|
||||
*/
|
||||
attribute ACString defaultURL;
|
||||
|
||||
/**
|
||||
* Returns true if the default resource got overridden.
|
||||
*/
|
||||
|
@ -3,3 +3,4 @@ support-files =
|
||||
dummy_page.html
|
||||
|
||||
[browser_remotenewtab_pageloads.js]
|
||||
[browser_newtab_overrides.js]
|
||||
|
@ -0,0 +1,139 @@
|
||||
/*globals
|
||||
XPCOMUtils,
|
||||
aboutNewTabService,
|
||||
Services,
|
||||
ContentTask,
|
||||
TestUtils,
|
||||
BrowserOpenTab,
|
||||
registerCleanupFunction,
|
||||
is,
|
||||
content
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
let Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
Services.prefs.setBoolPref("browser.newtabpage.remote", false);
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
});
|
||||
|
||||
/*
|
||||
* Tests that the default newtab page is always returned when one types "about:newtab" in the URL bar,
|
||||
* even when overridden.
|
||||
*/
|
||||
add_task(function* redirector_ignores_override() {
|
||||
let overrides = [
|
||||
"chrome://browser/content/downloads/contentAreaDownloadsView.xul",
|
||||
"about:home",
|
||||
];
|
||||
|
||||
for (let overrideURL of overrides) {
|
||||
let notificationPromise = nextChangeNotificationPromise(overrideURL, `newtab page now points to ${overrideURL}`);
|
||||
aboutNewTabService.newTabURL = overrideURL;
|
||||
|
||||
yield notificationPromise;
|
||||
Assert.ok(aboutNewTabService.overridden, "url has been overridden");
|
||||
|
||||
let tabOptions = {
|
||||
gBrowser,
|
||||
url: "about:newtab",
|
||||
};
|
||||
|
||||
/*
|
||||
* Simulate typing "about:newtab" in the url bar.
|
||||
*
|
||||
* Bug 1240169 - We expect the redirector to lead the user to "about:newtab", the default URL,
|
||||
* due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead
|
||||
* to the overriding URLs.
|
||||
*/
|
||||
yield BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
|
||||
yield ContentTask.spawn(browser, {}, function*() {
|
||||
is(content.location.href, "about:newtab", "Got right URL");
|
||||
is(content.document.location.href, "about:newtab", "Got right URL");
|
||||
is(content.document.nodePrincipal,
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
"nodePrincipal should match systemPrincipal");
|
||||
});
|
||||
}); // jshint ignore:line
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Tests loading an overridden newtab page by simulating opening a newtab page from chrome
|
||||
*/
|
||||
add_task(function* override_loads_in_browser() {
|
||||
let overrides = [
|
||||
"chrome://browser/content/downloads/contentAreaDownloadsView.xul",
|
||||
"about:home",
|
||||
" about:home",
|
||||
];
|
||||
|
||||
for (let overrideURL of overrides) {
|
||||
let notificationPromise = nextChangeNotificationPromise(overrideURL.trim(), `newtab page now points to ${overrideURL}`);
|
||||
aboutNewTabService.newTabURL = overrideURL;
|
||||
|
||||
yield notificationPromise;
|
||||
Assert.ok(aboutNewTabService.overridden, "url has been overridden");
|
||||
|
||||
// simulate a newtab open as a user would
|
||||
BrowserOpenTab(); // jshint ignore:line
|
||||
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
yield BrowserTestUtils.browserLoaded(browser);
|
||||
|
||||
yield ContentTask.spawn(browser, {url: overrideURL}, function*(args) {
|
||||
is(content.location.href, args.url.trim(), "Got right URL");
|
||||
is(content.document.location.href, args.url.trim(), "Got right URL");
|
||||
}); // jshint ignore:line
|
||||
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Tests edge cases when someone overrides the newtabpage with whitespace
|
||||
*/
|
||||
add_task(function* override_blank_loads_in_browser() {
|
||||
let overrides = [
|
||||
"",
|
||||
" ",
|
||||
"\n\t",
|
||||
" about:blank",
|
||||
];
|
||||
|
||||
for (let overrideURL of overrides) {
|
||||
let notificationPromise = nextChangeNotificationPromise("about:blank", "newtab page now points to about:blank");
|
||||
aboutNewTabService.newTabURL = overrideURL;
|
||||
|
||||
yield notificationPromise;
|
||||
Assert.ok(aboutNewTabService.overridden, "url has been overridden");
|
||||
|
||||
// simulate a newtab open as a user would
|
||||
BrowserOpenTab(); // jshint ignore:line
|
||||
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
yield BrowserTestUtils.browserLoaded(browser);
|
||||
|
||||
yield ContentTask.spawn(browser, {}, function*() {
|
||||
is(content.location.href, "about:blank", "Got right URL");
|
||||
is(content.document.location.href, "about:blank", "Got right URL");
|
||||
}); // jshint ignore:line
|
||||
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
}
|
||||
});
|
||||
|
||||
function nextChangeNotificationPromise(aNewURL, testMessage) {
|
||||
return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
|
||||
Assert.equal(aData, aNewURL, testMessage);
|
||||
return true;
|
||||
}.bind(this));
|
||||
}
|
@ -1,19 +1,18 @@
|
||||
/* globals XPCOMUtils, aboutNewTabService, Services */
|
||||
/* globals Cu, XPCOMUtils, TestUtils, aboutNewTabService, ContentTask, content, is */
|
||||
"use strict";
|
||||
|
||||
let Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RemotePageManager",
|
||||
"resource://gre/modules/RemotePageManager.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
|
||||
const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/dummy_page.html";
|
||||
|
||||
/*
|
||||
* Tests opening a newtab page with a remote URL. Simulates a newtab open from chrome
|
||||
*/
|
||||
add_task(function* open_newtab() {
|
||||
let notificationPromise = nextChangeNotificationPromise(TEST_URL, "newtab page now points to test url");
|
||||
aboutNewTabService.newTabURL = TEST_URL;
|
||||
@ -21,37 +20,30 @@ add_task(function* open_newtab() {
|
||||
yield notificationPromise;
|
||||
Assert.ok(aboutNewTabService.overridden, "url has been overridden");
|
||||
|
||||
let tabOptions = {
|
||||
gBrowser,
|
||||
url: "about:newtab",
|
||||
};
|
||||
/*
|
||||
* Simulate a newtab open as a user would.
|
||||
*
|
||||
* Bug 1240169 - We cannot set the URL to about:newtab because that would invoke the redirector.
|
||||
* The redirector always yields the loading of a default newtab URL. We expect the user to use
|
||||
* the browser UI to access overriding URLs, for istance by click on the "+" button in the tab
|
||||
* bar, or by using the new tab shortcut key.
|
||||
*/
|
||||
BrowserOpenTab(); // jshint ignore:line
|
||||
|
||||
yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) {
|
||||
Assert.equal(TEST_URL, browser.contentWindow.location, `New tab should open to ${TEST_URL}`);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* emptyURL() {
|
||||
let notificationPromise = nextChangeNotificationPromise("", "newtab service now points to empty url");
|
||||
aboutNewTabService.newTabURL = "";
|
||||
yield notificationPromise;
|
||||
|
||||
let tabOptions = {
|
||||
gBrowser,
|
||||
url: "about:newtab",
|
||||
};
|
||||
|
||||
yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) {
|
||||
Assert.equal("about:blank", browser.contentWindow.location, `New tab should open to ${"about:blank"}`);
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
yield BrowserTestUtils.browserLoaded(browser);
|
||||
|
||||
yield ContentTask.spawn(browser, {url: TEST_URL}, function*(args) {
|
||||
is(content.document.location.href, args.url, "document.location should match the external resource");
|
||||
is(content.document.documentURI, args.url, "document.documentURI should match the external resource");
|
||||
is(content.document.nodePrincipal.URI.spec, args.url, "nodePrincipal should match the external resource");
|
||||
});
|
||||
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
||||
|
||||
function nextChangeNotificationPromise(aNewURL, testMessage) {
|
||||
return new Promise(resolve => {
|
||||
Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
|
||||
Services.obs.removeObserver(observer, aTopic);
|
||||
return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
|
||||
Assert.equal(aData, aNewURL, testMessage);
|
||||
resolve();
|
||||
}, "newtab-url-changed", false);
|
||||
});
|
||||
return true;
|
||||
}.bind(this));
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
/* globals Services, XPCOMUtils, NewTabPrefsProvider, Preferences, aboutNewTabService */
|
||||
/* globals Services, XPCOMUtils, NewTabPrefsProvider, Preferences, aboutNewTabService, do_register_cleanup */
|
||||
|
||||
"use strict";
|
||||
|
||||
@ -19,25 +19,30 @@ XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"nsIAboutNewTabService");
|
||||
|
||||
const DEFAULT_HREF = aboutNewTabService.generateRemoteURL();
|
||||
const DEFAULT_CHROME_URL = "chrome://browser/content/newtab/newTab.xhtml";
|
||||
const DOWNLOADS_URL = "chrome://browser/content/downloads/contentAreaDownloadsView.xul";
|
||||
|
||||
function cleanup() {
|
||||
Services.prefs.setBoolPref("browser.newtabpage.remote", false);
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
NewTabPrefsProvider.prefs.uninit();
|
||||
}
|
||||
|
||||
do_register_cleanup(cleanup);
|
||||
|
||||
/**
|
||||
* Test the overriding of the default URL
|
||||
*/
|
||||
add_task(function* () {
|
||||
add_task(function* test_override_remote_disabled() {
|
||||
NewTabPrefsProvider.prefs.init();
|
||||
let notificationPromise;
|
||||
Services.prefs.setBoolPref("browser.newtabpage.remote", false);
|
||||
let localChromeURL = "chrome://browser/content/newtab/newTab.xhtml";
|
||||
|
||||
// tests default is the local newtab resource
|
||||
Assert.equal(aboutNewTabService.newTabURL, localChromeURL,
|
||||
`Default newtab URL should be ${localChromeURL}`);
|
||||
|
||||
// test the newtab service does not go in a circular redirect
|
||||
aboutNewTabService.newTabURL = "about:newtab";
|
||||
Assert.equal(aboutNewTabService.newTabURL, localChromeURL,
|
||||
"Newtab URL avoids a circular redirect by setting to the default URL");
|
||||
Assert.equal(aboutNewTabService.defaultURL, DEFAULT_CHROME_URL,
|
||||
`Default newtab URL should be ${DEFAULT_CHROME_URL}`);
|
||||
|
||||
// override with some remote URL
|
||||
let url = "http://example.com/";
|
||||
notificationPromise = nextChangeNotificationPromise(url);
|
||||
aboutNewTabService.newTabURL = url;
|
||||
@ -46,27 +51,59 @@ add_task(function* () {
|
||||
Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
|
||||
Assert.equal(aboutNewTabService.newTabURL, url, "Newtab URL should be the custom URL");
|
||||
|
||||
notificationPromise = nextChangeNotificationPromise("chrome://browser/content/newtab/newTab.xhtml");
|
||||
// test reset with remote disabled
|
||||
notificationPromise = nextChangeNotificationPromise("about:newtab");
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
yield notificationPromise;
|
||||
Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
|
||||
Assert.equal(aboutNewTabService.newTabURL, "chrome://browser/content/newtab/newTab.xhtml",
|
||||
"Newtab URL should be the default");
|
||||
Assert.equal(aboutNewTabService.newTabURL, "about:newtab", "Newtab URL should be the default");
|
||||
|
||||
// test override to a chrome URL
|
||||
notificationPromise = nextChangeNotificationPromise(DOWNLOADS_URL);
|
||||
aboutNewTabService.newTabURL = DOWNLOADS_URL;
|
||||
yield notificationPromise;
|
||||
Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
|
||||
Assert.equal(aboutNewTabService.newTabURL, DOWNLOADS_URL, "Newtab URL should be the custom URL");
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
add_task(function* test_override_remote_enabled() {
|
||||
NewTabPrefsProvider.prefs.init();
|
||||
let notificationPromise;
|
||||
// change newtab page to remote
|
||||
notificationPromise = nextChangeNotificationPromise("about:newtab");
|
||||
Services.prefs.setBoolPref("browser.newtabpage.remote", true);
|
||||
yield notificationPromise;
|
||||
let remoteHref = aboutNewTabService.generateRemoteURL();
|
||||
Assert.equal(aboutNewTabService.newTabURL, remoteHref, "Newtab URL should be the default remote URL");
|
||||
Assert.equal(aboutNewTabService.defaultURL, remoteHref, "Newtab URL should be the default remote URL");
|
||||
Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
|
||||
Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
|
||||
NewTabPrefsProvider.prefs.uninit();
|
||||
|
||||
// change to local newtab page while remote is enabled
|
||||
notificationPromise = nextChangeNotificationPromise(DEFAULT_CHROME_URL);
|
||||
aboutNewTabService.newTabURL = DEFAULT_CHROME_URL;
|
||||
yield notificationPromise;
|
||||
Assert.equal(aboutNewTabService.newTabURL, DEFAULT_CHROME_URL,
|
||||
"Newtab URL set to chrome url");
|
||||
Assert.equal(aboutNewTabService.defaultURL, DEFAULT_CHROME_URL,
|
||||
"Newtab URL defaultURL set to the default chrome URL");
|
||||
Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
|
||||
Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests reponse to updates to prefs
|
||||
*/
|
||||
add_task(function* test_updates() {
|
||||
/*
|
||||
* Simulates a "cold-boot" situation, with some pref already set before testing a series
|
||||
* of changes.
|
||||
*/
|
||||
Preferences.set("browser.newtabpage.remote", true);
|
||||
aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off
|
||||
let notificationPromise;
|
||||
let expectedHref = "https://newtab.cdn.mozilla.net" +
|
||||
`/v${aboutNewTabService.remoteVersion}` +
|
||||
@ -98,9 +135,10 @@ add_task(function* test_updates() {
|
||||
|
||||
// from overridden to default
|
||||
notificationPromise = nextChangeNotificationPromise(
|
||||
DEFAULT_HREF, "a notification occurs on reset");
|
||||
"about:newtab", "a notification occurs on reset");
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
|
||||
Assert.equal(aboutNewTabService.defaultURL, DEFAULT_HREF, "Default URL should be the remote page");
|
||||
yield notificationPromise;
|
||||
|
||||
// override to default URL from default URL
|
||||
@ -109,16 +147,16 @@ add_task(function* test_updates() {
|
||||
aboutNewTabService.newTabURL = aboutNewTabService.generateRemoteURL();
|
||||
Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
|
||||
aboutNewTabService.newTabURL = testURL;
|
||||
Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
|
||||
yield notificationPromise;
|
||||
Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
|
||||
|
||||
// reset twice, only one notification for default URL
|
||||
notificationPromise = nextChangeNotificationPromise(
|
||||
DEFAULT_HREF, "reset occurs");
|
||||
"about:newtab", "reset occurs");
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
yield notificationPromise;
|
||||
|
||||
NewTabPrefsProvider.prefs.uninit();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -2,7 +2,7 @@
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
/* globals Services, NewTabURL, XPCOMUtils, aboutNewTabService */
|
||||
/* globals Services, NewTabURL, XPCOMUtils, aboutNewTabService, NewTabPrefsProvider */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
@ -15,7 +15,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
|
||||
add_task(function* () {
|
||||
add_task(function*() {
|
||||
let defaultURL = aboutNewTabService.newTabURL;
|
||||
Services.prefs.setBoolPref("browser.newtabpage.remote", false);
|
||||
|
||||
@ -36,8 +36,7 @@ add_task(function* () {
|
||||
// change newtab page to remote
|
||||
NewTabPrefsProvider.prefs.init();
|
||||
Services.prefs.setBoolPref("browser.newtabpage.remote", true);
|
||||
let remoteURL = aboutNewTabService.generateRemoteURL();
|
||||
Assert.equal(NewTabURL.get(), remoteURL, `Newtab URL should be ${remoteURL}`);
|
||||
Assert.equal(NewTabURL.get(), "about:newtab", `Newtab URL should be about:newtab`);
|
||||
Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
|
||||
NewTabPrefsProvider.prefs.uninit();
|
||||
});
|
||||
|
45
browser/components/syncedtabs/EventEmitter.jsm
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
165
browser/components/syncedtabs/SyncedTabsDeckComponent.js
Normal file
@ -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(null, "tabs-sidebar");
|
||||
}
|
||||
};
|
||||
|
60
browser/components/syncedtabs/SyncedTabsDeckStore.js
Normal file
@ -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();
|
||||
}
|
||||
});
|
111
browser/components/syncedtabs/SyncedTabsDeckView.js
Normal file
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
228
browser/components/syncedtabs/SyncedTabsListStore.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
|
114
browser/components/syncedtabs/TabListComponent.js
Normal file
@ -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);
|
||||
}
|
||||
};
|
438
browser/components/syncedtabs/TabListView.js
Normal file
@ -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");
|
||||
}
|
||||
};
|
7
browser/components/syncedtabs/jar.mn
Normal file
@ -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
|
24
browser/components/syncedtabs/moz.build
Normal file
@ -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')
|
||||
|
30
browser/components/syncedtabs/sidebar.js
Normal file
@ -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);
|
99
browser/components/syncedtabs/sidebar.xhtml
Normal file
@ -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>
|
4
browser/components/syncedtabs/test/browser/browser.ini
Normal file
@ -0,0 +1,4 @@
|
||||
[DEFAULT]
|
||||
support-files = head.js
|
||||
|
||||
[browser_sidebar_syncedtabslist.js]
|
@ -0,0 +1,262 @@
|
||||
"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;
|
||||
|
||||
yield new Promise(resolve => {
|
||||
window.SidebarUI.browser.contentWindow.addEventListener("unload", function listener() {
|
||||
window.SidebarUI.browser.contentWindow.removeEventListener("unload", listener);
|
||||
resolve();
|
||||
});
|
||||
SidebarUI.hide();
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
18
browser/components/syncedtabs/test/browser/head.js
Normal file
@ -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;
|
||||
});
|
29
browser/components/syncedtabs/test/xpcshell/head.js
Normal file
@ -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();
|
||||
});
|
||||
|
10
browser/components/syncedtabs/test/xpcshell/xpcshell.ini
Normal file
@ -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]
|
23
browser/components/syncedtabs/util.js
Normal file
@ -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;…">
|
||||
@ -928,7 +947,3 @@ you can use these alternative items. Otherwise, their values should be empty. -
|
||||
|
||||
<!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…">
|
||||
<!ENTITY emeLearnMoreContextMenu.accesskey "D">
|
||||
<!ENTITY emeNotificationsNotNow.label "Not now">
|
||||
<!ENTITY emeNotificationsNotNow.accesskey "N">
|
||||
<!ENTITY emeNotificationsDontAskAgain.label "Don't ask me again">
|
||||
<!ENTITY emeNotificationsDontAskAgain.accesskey "D">
|
||||
|
@ -687,14 +687,9 @@ emeNotifications.drmContentCDMInsufficientVersion.message = %S is installing upd
|
||||
# LOCALIZATION NOTE(emeNotifications.drmContentCDMInstalling.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %S is brandShortName
|
||||
emeNotifications.drmContentCDMInstalling.message = %S is installing components needed to play the audio or video on this page. Please try again later.
|
||||
|
||||
# LOCALIZATION NOTE(emeNotifications.drmContentCDMNotSupported.64bit.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %1$S is brandShortName, %2$S will be the 'learn more' link
|
||||
emeNotifications.drmContentCDMNotSupported.64bit.message = The audio or video on this page requires DRM software that this 64-bit build of %1$S does not support. %2$S
|
||||
# LOCALIZATION NOTE(emeNotifications.drmContentCDMNotSupported.unsupportedOS.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %1$S is brandShortName, %2$S is the name of the user's OS (Windows, Linux, Mac OS X), %3$S will be the 'learn more' link
|
||||
emeNotifications.drmContentCDMNotSupported.unsupportedOS.message = The audio or video on this page requires DRM software that %1$S does not support on %2$S. %3$S
|
||||
|
||||
emeNotifications.optionsButton.label = Options
|
||||
emeNotifications.optionsButton.accesskey = O
|
||||
|
||||
emeNotifications.unknownDRMSoftware = Unknown
|
||||
|
||||
# LOCALIZATION NOTE - %S is brandShortName
|
||||
@ -807,4 +802,3 @@ certErrorDetailsCertChain.label = Certificate chain:
|
||||
# %S is the group number/ID
|
||||
tabgroups.migration.anonGroup = Group %S
|
||||
tabgroups.migration.tabGroupBookmarkFolderName = Bookmarked Tab Groups
|
||||
|
||||
|
@ -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
|
||||
|
58
browser/themes/linux/syncedtabs/sidebar.css
Normal file
@ -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;
|
||||
@ -53,7 +54,7 @@
|
||||
border-top: @sidebarItemGraphiteFocusedBorderTop@;
|
||||
}
|
||||
|
||||
.sidebar-placesTreechildren::-moz-tree-cell-text(selected) {
|
||||
.sidebar-placesTreechildren::-moz-tree-cell-text(selected) {
|
||||
font-weight: bold !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 1.3 KiB |
7
browser/themes/osx/syncedtabs/arrow-closed.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 472 B |
7
browser/themes/osx/syncedtabs/arrow-open.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 477 B |
96
browser/themes/osx/syncedtabs/sidebar.css
Normal file
@ -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;
|
||||
}
|
@ -83,6 +83,8 @@
|
||||
skin/classic/browser/fxa/android@2x.png (../shared/fxa/android@2x.png)
|
||||
skin/classic/browser/fxa/ios.png (../shared/fxa/ios.png)
|
||||
skin/classic/browser/fxa/ios@2x.png (../shared/fxa/ios@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)
|
||||
|
199
browser/themes/shared/syncedtabs/sidebar.inc.css
Normal file
@ -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;
|
||||
}
|
59
browser/themes/shared/syncedtabs/twisty-closed.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1.9 KiB |
62
browser/themes/shared/syncedtabs/twisty-open.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 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;
|
||||
}
|
||||
}
|
||||
|
103
browser/themes/windows/syncedtabs/sidebar.css
Normal file
@ -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;
|
||||
}
|
||||
|
@ -748,6 +748,7 @@ media/stagefright/foundation/hexdump.h
|
||||
media/stagefright/MediaBuffer.h
|
||||
media/stagefright/MediaBufferGroup.h
|
||||
media/stagefright/MediaCodec.h
|
||||
media/stagefright/MediaCodecList.h
|
||||
media/stagefright/MediaCodecSource.h
|
||||
media/stagefright/MediaDefs.h
|
||||
media/stagefright/MediaErrors.h
|
||||
|
@ -5,7 +5,9 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
|
||||
TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
|
||||
'test/shared-redux-head.js',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'attach-thread.js',
|
||||
|
@ -1,7 +0,0 @@
|
||||
[DEFAULT]
|
||||
tags = devtools
|
||||
head =
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
support-files =
|
||||
shared-redux-head.js
|
@ -4,7 +4,5 @@
|
||||
# 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_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
|
||||
DevToolsModules(
|
||||
)
|
||||
|
@ -1,5 +0,0 @@
|
||||
[DEFAULT]
|
||||
tags = devtools
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
head.js
|
@ -85,7 +85,7 @@ addTest(function inheritedSystemStyles() {
|
||||
ok(!applied[1].rule.parentStyleSheet.system, "Entry 1 should be a system style");
|
||||
is(applied[1].rule.type, 1, "Entry 1 should be a rule style");
|
||||
|
||||
is(applied.length, 11, "Should have 11 rules.");
|
||||
is(applied.length, 12, "Should have 12 rules.");
|
||||
}).then(runNextTest));
|
||||
});
|
||||
|
||||
|
@ -4,10 +4,18 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'tests/data/mochitest.ini',
|
||||
'tests/mochitest.ini',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.tests.devtools.shared.apps.tests.data += [
|
||||
'tests/data/app-certified.zip',
|
||||
'tests/data/app-overload.zip',
|
||||
'tests/data/app-redirect.zip',
|
||||
'tests/data/app-system.zip',
|
||||
'tests/data/app-updated.zip',
|
||||
'tests/data/app.zip',
|
||||
]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
|
||||
|
||||
DevToolsModules(
|
||||
|
@ -1,8 +0,0 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
app-redirect.zip
|
||||
app-updated.zip
|
||||
app.zip
|
||||
app-certified.zip
|
||||
app-overload.zip
|
||||
app-system.zip
|
@ -134,7 +134,7 @@ static RedirEntry kRedirMap[] = {
|
||||
nsIAboutModule::ALLOW_SCRIPT
|
||||
},
|
||||
{
|
||||
"webrtc", "chrome://global/content/aboutwebrtc/aboutWebrtc.xhtml",
|
||||
"webrtc", "chrome://global/content/aboutwebrtc/aboutWebrtc.html",
|
||||
nsIAboutModule::ALLOW_SCRIPT
|
||||
}
|
||||
};
|
||||
|
@ -17,7 +17,6 @@ XPCSHELL_TESTS_MANIFESTS += [
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'test/chrome/mochitest.ini',
|
||||
'test/iframesandbox/mochitest.ini',
|
||||
'test/mochitest.ini',
|
||||
'test/navigation/mochitest.ini',
|
||||
@ -31,3 +30,21 @@ BROWSER_CHROME_MANIFESTS += [
|
||||
'test/browser/browser.ini',
|
||||
'test/navigation/browser.ini',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.tests.docshell.test.chrome += [
|
||||
'test/chrome/112564_nocache.html',
|
||||
'test/chrome/112564_nocache.html^headers^',
|
||||
'test/chrome/215405_nocache.html',
|
||||
'test/chrome/215405_nocache.html^headers^',
|
||||
'test/chrome/215405_nostore.html',
|
||||
'test/chrome/215405_nostore.html^headers^',
|
||||
'test/chrome/582176_dummy.html',
|
||||
'test/chrome/582176_xml.xml',
|
||||
'test/chrome/582176_xslt.xsl',
|
||||
'test/chrome/92598_nostore.html',
|
||||
'test/chrome/92598_nostore.html^headers^',
|
||||
'test/chrome/allowContentRetargeting.sjs',
|
||||
'test/chrome/blue.png',
|
||||
'test/chrome/bug89419.sjs',
|
||||
'test/chrome/red.png',
|
||||
]
|
||||
|
@ -1,17 +0,0 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
bug89419.sjs
|
||||
blue.png
|
||||
red.png
|
||||
92598_nostore.html
|
||||
92598_nostore.html^headers^
|
||||
112564_nocache.html
|
||||
112564_nocache.html^headers^
|
||||
215405_nostore.html
|
||||
215405_nostore.html^headers^
|
||||
215405_nocache.html
|
||||
215405_nocache.html^headers^
|
||||
582176_dummy.html
|
||||
582176_xml.xml
|
||||
582176_xslt.xsl
|
||||
allowContentRetargeting.sjs
|
@ -1,8 +0,0 @@
|
||||
# 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/.
|
||||
|
||||
INSTALL_TARGETS += usecounterlist
|
||||
usecounterlist_FILES := UseCounterList.h
|
||||
usecounterlist_DEST = $(DIST)/include/mozilla/dom
|
||||
usecounterlist_TARGET := export
|
@ -74,14 +74,33 @@ WebKitCSSMatrix::Multiply(const WebKitCSSMatrix& other) const
|
||||
}
|
||||
|
||||
already_AddRefed<WebKitCSSMatrix>
|
||||
WebKitCSSMatrix::Inverse() const
|
||||
WebKitCSSMatrix::Inverse(ErrorResult& aRv) const
|
||||
{
|
||||
RefPtr<WebKitCSSMatrix> retval = new WebKitCSSMatrix(mParent, *this);
|
||||
retval->InvertSelf();
|
||||
retval->InvertSelfThrow(aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return retval.forget();
|
||||
}
|
||||
|
||||
WebKitCSSMatrix*
|
||||
WebKitCSSMatrix::InvertSelfThrow(ErrorResult& aRv)
|
||||
{
|
||||
if (mMatrix3D) {
|
||||
if (!mMatrix3D->Invert()) {
|
||||
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
||||
return nullptr;
|
||||
}
|
||||
} else if (!mMatrix2D->Invert()) {
|
||||
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
already_AddRefed<WebKitCSSMatrix>
|
||||
WebKitCSSMatrix::Translate(double aTx,
|
||||
double aTy,
|
||||
|
@ -42,7 +42,7 @@ public:
|
||||
ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<WebKitCSSMatrix> Multiply(const WebKitCSSMatrix& aOther) const;
|
||||
already_AddRefed<WebKitCSSMatrix> Inverse() const;
|
||||
already_AddRefed<WebKitCSSMatrix> Inverse(ErrorResult& aRv) const;
|
||||
already_AddRefed<WebKitCSSMatrix> Translate(double aTx,
|
||||
double aTy,
|
||||
double aTz) const;
|
||||
@ -62,6 +62,8 @@ protected:
|
||||
WebKitCSSMatrix* Rotate3dSelf(double aRotX,
|
||||
double aRotY,
|
||||
double aRotZ);
|
||||
|
||||
WebKitCSSMatrix* InvertSelfThrow(ErrorResult& aRv);
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
@ -150,6 +150,7 @@ EXPORTS.mozilla += [
|
||||
]
|
||||
|
||||
EXPORTS.mozilla.dom += [
|
||||
'!UseCounterList.h',
|
||||
'AnonymousContent.h',
|
||||
'Attr.h',
|
||||
'BarProps.h',
|
||||
|
@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
#include "nsDocument.h"
|
||||
|
||||
#include "nsIDocumentInlines.h"
|
||||
#include "mozilla/AnimationComparator.h"
|
||||
#include "mozilla/ArrayUtils.h"
|
||||
#include "mozilla/AutoRestore.h"
|
||||
@ -10731,6 +10731,54 @@ nsDocument::CaretPositionFromPoint(float aX, float aY, nsISupports** aCaretPos)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
static bool
|
||||
IsPotentiallyScrollable(HTMLBodyElement* aBody)
|
||||
{
|
||||
// An element is potentially scrollable if all of the following conditions are
|
||||
// true:
|
||||
|
||||
// The element has an associated CSS layout box.
|
||||
nsIFrame* bodyFrame = aBody->GetPrimaryFrame();
|
||||
if (!bodyFrame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The element is not the HTML body element, or it is and the root element's
|
||||
// used value of the overflow-x or overflow-y properties is not visible.
|
||||
MOZ_ASSERT(aBody->GetParent() == aBody->OwnerDoc()->GetRootElement());
|
||||
nsIFrame* parentFrame = aBody->GetParent()->GetPrimaryFrame();
|
||||
if (parentFrame &&
|
||||
parentFrame->StyleDisplay()->mOverflowX == NS_STYLE_OVERFLOW_VISIBLE &&
|
||||
parentFrame->StyleDisplay()->mOverflowY == NS_STYLE_OVERFLOW_VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The element's used value of the overflow-x or overflow-y properties is not
|
||||
// visible.
|
||||
if (bodyFrame->StyleDisplay()->mOverflowX == NS_STYLE_OVERFLOW_VISIBLE &&
|
||||
bodyFrame->StyleDisplay()->mOverflowY == NS_STYLE_OVERFLOW_VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Element*
|
||||
nsIDocument::GetScrollingElement()
|
||||
{
|
||||
if (GetCompatibilityMode() == eCompatibility_NavQuirks) {
|
||||
FlushPendingNotifications(Flush_Layout);
|
||||
HTMLBodyElement* body = GetBodyElement();
|
||||
if (body && !IsPotentiallyScrollable(body)) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return GetRootElement();
|
||||
}
|
||||
|
||||
void
|
||||
nsIDocument::ObsoleteSheet(nsIURI *aSheetURI, ErrorResult& rv)
|
||||
{
|
||||
|
@ -126,6 +126,7 @@ GK_ATOM(autoplay, "autoplay")
|
||||
GK_ATOM(autorepeatbutton, "autorepeatbutton")
|
||||
GK_ATOM(axis, "axis")
|
||||
GK_ATOM(b, "b")
|
||||
GK_ATOM(backdropFrame, "BackdropFrame")
|
||||
GK_ATOM(background, "background")
|
||||
GK_ATOM(base, "base")
|
||||
GK_ATOM(basefont, "basefont")
|
||||
|
@ -2544,6 +2544,8 @@ public:
|
||||
already_AddRefed<nsDOMCaretPosition>
|
||||
CaretPositionFromPoint(float aX, float aY);
|
||||
|
||||
Element* GetScrollingElement();
|
||||
|
||||
// QuerySelector and QuerySelectorAll already defined on nsINode
|
||||
nsINodeList* GetAnonymousNodes(Element& aElement);
|
||||
Element* GetAnonymousElementByAttribute(Element& aElement,
|
||||
|
@ -294,7 +294,9 @@ nsScriptLoader::StartLoad(nsScriptLoadRequest *aRequest, const nsAString &aType,
|
||||
aRequest->mCORSMode == CORS_NONE
|
||||
? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
|
||||
: nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
|
||||
if (aRequest->mCORSMode == CORS_USE_CREDENTIALS) {
|
||||
if (aRequest->mCORSMode == CORS_ANONYMOUS) {
|
||||
securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
|
||||
} else if (aRequest->mCORSMode == CORS_USE_CREDENTIALS) {
|
||||
securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
|
||||
}
|
||||
securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME;
|
||||
@ -643,21 +645,6 @@ nsScriptLoader::ProcessScriptElement(nsIScriptElement *aElement)
|
||||
// sheets. If the script comes from the network stream, cheat for
|
||||
// performance reasons and avoid a trip through the event loop.
|
||||
if (aElement->GetParserCreated() == FROM_PARSER_NETWORK) {
|
||||
// Attempt to compile script off-thread first -- it reduces locking of
|
||||
// the main thread for long time.
|
||||
if (NumberOfProcessors() > 1 &&
|
||||
AttemptAsyncScriptCompile(request) == NS_OK) {
|
||||
NS_ASSERTION(request->mProgress == nsScriptLoadRequest::Progress_Compiling,
|
||||
"Request should be off-thread compiling now.");
|
||||
NS_ASSERTION(!mParserBlockingRequest,
|
||||
"There can be only one parser-blocking script at a time");
|
||||
NS_ASSERTION(mXSLTRequests.isEmpty(),
|
||||
"Parser-blocking scripts and XSLT scripts in the same doc!");
|
||||
mParserBlockingRequest = request;
|
||||
return true;
|
||||
}
|
||||
// And process a request if off-thread compilation heuristics think that
|
||||
// non-async way will be faster.
|
||||
return ProcessRequest(request) == NS_ERROR_HTMLPARSER_BLOCK;
|
||||
}
|
||||
// Otherwise, we've got a document.written script, make a trip through
|
||||
|
@ -1,6 +0,0 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
bug421622-referer.sjs
|
||||
nochrome_bug765993.html
|
||||
nochrome_bug765993.js
|
||||
nochrome_bug765993.js^headers^
|
@ -16,7 +16,6 @@ GeckoCppUnitTests([
|
||||
])
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'chrome/mochitest.ini',
|
||||
'mochitest.ini',
|
||||
'websocket_hybi/mochitest.ini',
|
||||
]
|
||||
@ -40,3 +39,10 @@ BROWSER_CHROME_MANIFESTS += [
|
||||
TEST_DIRS += [
|
||||
'gtest',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.tests.dom.base.test.chrome += [
|
||||
'chrome/bug421622-referer.sjs',
|
||||
'chrome/nochrome_bug765993.html',
|
||||
'chrome/nochrome_bug765993.js',
|
||||
'chrome/nochrome_bug765993.js^headers^',
|
||||
]
|
||||
|
1
dom/cache/CacheStorage.cpp
vendored
@ -119,7 +119,6 @@ IsTrusted(const PrincipalInfo& aPrincipalInfo, bool aTestingPrefEnabled)
|
||||
|
||||
nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen));
|
||||
if (scheme.LowerCaseEqualsLiteral("https") ||
|
||||
scheme.LowerCaseEqualsLiteral("app") ||
|
||||
scheme.LowerCaseEqualsLiteral("file")) {
|
||||
return true;
|
||||
}
|
||||
|
3
dom/cache/TypeUtils.cpp
vendored
@ -417,8 +417,7 @@ TypeUtils::ProcessURL(nsACString& aUrl, bool* aSchemeValidOut,
|
||||
if (aSchemeValidOut) {
|
||||
nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen));
|
||||
*aSchemeValidOut = scheme.LowerCaseEqualsLiteral("http") ||
|
||||
scheme.LowerCaseEqualsLiteral("https") ||
|
||||
scheme.LowerCaseEqualsLiteral("app");
|
||||
scheme.LowerCaseEqualsLiteral("https");
|
||||
}
|
||||
|
||||
uint32_t queryPos;
|
||||
|
@ -1065,7 +1065,7 @@ private:
|
||||
RefPtr<layers::Image> data = DecodeAndCropBlob(*mBlob, mCropRect);
|
||||
|
||||
if (NS_WARN_IF(!data)) {
|
||||
mPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
|
||||
mPromise->MaybeRejectWithNull();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -1143,9 +1143,9 @@ private:
|
||||
mPromise->MaybeReject(rv);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
if (NS_WARN_IF(!data)) {
|
||||
mPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
|
||||
mPromise->MaybeRejectWithNull();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -212,10 +212,6 @@ WebGL2Context::IsTexParamValid(GLenum pname) const
|
||||
case LOCAL_GL_TEXTURE_IMMUTABLE_FORMAT:
|
||||
case LOCAL_GL_TEXTURE_IMMUTABLE_LEVELS:
|
||||
case LOCAL_GL_TEXTURE_MAX_LEVEL:
|
||||
case LOCAL_GL_TEXTURE_SWIZZLE_A:
|
||||
case LOCAL_GL_TEXTURE_SWIZZLE_B:
|
||||
case LOCAL_GL_TEXTURE_SWIZZLE_G:
|
||||
case LOCAL_GL_TEXTURE_SWIZZLE_R:
|
||||
case LOCAL_GL_TEXTURE_WRAP_R:
|
||||
case LOCAL_GL_TEXTURE_MAX_LOD:
|
||||
case LOCAL_GL_TEXTURE_MIN_LOD:
|
||||
|
@ -275,14 +275,14 @@ STRONG_GLENUM_BEGIN(TexCompareMode)
|
||||
STRONG_GLENUM_END(TexCompareMode)
|
||||
|
||||
STRONG_GLENUM_BEGIN(TexCompareFunc)
|
||||
STRONG_GLENUM_VALUE(LEQUAL),
|
||||
STRONG_GLENUM_VALUE(GEQUAL),
|
||||
STRONG_GLENUM_VALUE(LESS),
|
||||
STRONG_GLENUM_VALUE(GREATER),
|
||||
STRONG_GLENUM_VALUE(EQUAL),
|
||||
STRONG_GLENUM_VALUE(NOTEQUAL),
|
||||
STRONG_GLENUM_VALUE(ALWAYS),
|
||||
STRONG_GLENUM_VALUE(NEVER),
|
||||
STRONG_GLENUM_VALUE(LESS),
|
||||
STRONG_GLENUM_VALUE(EQUAL),
|
||||
STRONG_GLENUM_VALUE(LEQUAL),
|
||||
STRONG_GLENUM_VALUE(GREATER),
|
||||
STRONG_GLENUM_VALUE(NOTEQUAL),
|
||||
STRONG_GLENUM_VALUE(GEQUAL),
|
||||
STRONG_GLENUM_VALUE(ALWAYS),
|
||||
STRONG_GLENUM_END(TexCompareFunc)
|
||||
|
||||
STRONG_GLENUM_BEGIN(TexFormat)
|
||||
|