Merge fx-team to m-c. a=merge

CLOSED TREE
This commit is contained in:
Ryan VanderMeulen 2015-02-27 13:21:37 -05:00
commit 753509e66c
67 changed files with 709 additions and 89 deletions

View File

@ -1718,15 +1718,17 @@ pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false); pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false); pref("loop.debug.sdk", false);
#ifdef DEBUG #ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:"); pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else #else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:"); pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#endif #endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto"); pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds"); pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
pref("loop.fxa_oauth.tokendata", ""); pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", ""); pref("loop.fxa_oauth.profile", "");
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc"); pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
pref("loop.contacts.gravatars.show", false);
pref("loop.contacts.gravatars.promo", true);
// serverURL to be assigned by services team // serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/"); pref("services.push.serverURL", "wss://push.services.mozilla.com/");

View File

@ -446,7 +446,9 @@
</menu> </menu>
#ifndef XP_MACOSX #ifndef XP_MACOSX
# Disabled on Mac because we can't fill native menupopups asynchronously # Disabled on Mac because we can't fill native menupopups asynchronously
<menuseparator/> <menuseparator id="menu_readingListSeparator">
<observes element="readingListSidebar" attribute="hidden"/>
</menuseparator>
<menu id="menu_readingList" <menu id="menu_readingList"
class="menu-iconic bookmark-item" class="menu-iconic bookmark-item"
label="&readingList.label;" label="&readingList.label;"

View File

@ -117,6 +117,15 @@ const cloneValueInto = function(value, targetWindow) {
return clone; return clone;
}; };
/**
* Get the two-digit hexadecimal code for a byte
*
* @param {byte} charCode
*/
const toHexString = function(charCode) {
return ("0" + charCode.toString(16)).slice(-2);
};
/** /**
* Inject any API containing _only_ function properties into the given window. * Inject any API containing _only_ function properties into the given window.
* *
@ -740,6 +749,43 @@ function injectLoopAPI(targetWindow) {
} }
}, },
/**
* Compose a URL pointing to the location of an avatar by email address.
* At the moment we use the Gravatar service to match email addresses with
* avatars. This might change in the future as avatars might come from another
* source.
*
* @param {String} emailAddress Users' email address
* @param {Number} size Size of the avatar image to return in pixels.
* Optional. Default value: 40.
* @return the URL pointing to an avatar matching the provided email address.
*/
getUserAvatar: {
enumerable: true,
writable: true,
value: function(emailAddress, size = 40) {
const kEmptyGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
if (!emailAddress || !MozLoopService.getLoopPref("contacts.gravatars.show")) {
return kEmptyGif;
}
// Do the MD5 dance.
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.MD5);
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stringStream.data = emailAddress.trim().toLowerCase();
hasher.updateFromStream(stringStream, -1);
let hash = hasher.finish(false);
// Convert the binary hash data to a hex string.
let md5Email = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
// Compose the Gravatar URL.
return "https://www.gravatar.com/avatar/" + md5Email + ".jpg?default=blank&s=" + size;
}
},
/** /**
* Associates a session-id and a call-id with a window for debugging. * Associates a session-id and a call-id with a window for debugging.
* *

View File

@ -229,3 +229,44 @@
.contact-form > .button-group { .contact-form > .button-group {
margin-top: 1rem; margin-top: 1rem;
} }
.contacts-gravatar-promo {
position: relative;
border: 1px dashed #c1c1c1;
border-radius: 2px;
background-color: #fbfbfb;
padding: 10px;
margin-top: 10px;
}
.contacts-gravatar-promo > p {
margin-top: 2px;
margin-bottom: 8px;
margin-right: 4px;
word-wrap: break-word;
}
body[dir=rtl] .contacts-gravatar-promo > p {
margin-right: 0;
margin-left: 4px;
}
.contacts-gravatar-promo > p > a {
color: #0295df;
text-decoration: none;
}
.contacts-gravatar-promo > p > a:hover {
text-decoration: underline;
}
.contacts-gravatar-promo > .button-close {
position: absolute;
top: 8px;
right: 8px;
}
body[dir=rtl] .contacts-gravatar-promo > .button-close {
right: auto;
left: 8px;
}

View File

@ -390,6 +390,23 @@ body {
color: #fff; color: #fff;
} }
.button-close {
background-color: transparent;
background-image: url(../shared/img/icons-10x10.svg#close);
background-repeat: no-repeat;
background-size: 8px 8px;
border: none;
padding: 0;
height: 8px;
width: 8px;
}
.button-close:hover,
.button-close:hover:active {
background-color: transparent;
border: none;
}
/* Dropdown menu */ /* Dropdown menu */
.dropdown { .dropdown {

View File

@ -81,6 +81,59 @@ loop.contacts = (function(_, mozL10n) {
contact[field][0].value = value; contact[field][0].value = value;
}; };
const GravatarPromo = React.createClass({displayName: "GravatarPromo",
propTypes: {
handleUse: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
!navigator.mozLoop.getLoopPref("contacts.gravatars.show")
};
},
handleCloseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
this.setState({ showMe: false });
},
handleUseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
this.setState({ showMe: false });
this.props.handleUse();
},
render: function() {
if (!this.state.showMe) {
return null;
}
let privacyUrl = navigator.mozLoop.getLoopPref("legal.privacy_url");
let message = mozL10n.get("gravatars_promo_message", {
"learn_more": React.renderToStaticMarkup(
React.createElement("a", {href: privacyUrl, target: "_blank"},
mozL10n.get("gravatars_promo_message_learnmore")
)
)
});
return (
React.createElement("div", {className: "contacts-gravatar-promo"},
React.createElement(Button, {additionalClass: "button-close", onClick: this.handleCloseButtonClick}),
React.createElement("p", {dangerouslySetInnerHTML: {__html: message}}),
React.createElement(ButtonGroup, null,
React.createElement(Button, {caption: mozL10n.get("gravatars_promo_button_nothanks"),
onClick: this.handleCloseButtonClick}),
React.createElement(Button, {caption: mozL10n.get("gravatars_promo_button_use"),
additionalClass: "button-accept",
onClick: this.handleUseButtonClick})
)
)
);
}
});
const ContactDropdown = React.createClass({displayName: "ContactDropdown", const ContactDropdown = React.createClass({displayName: "ContactDropdown",
propTypes: { propTypes: {
handleAction: React.PropTypes.func.isRequired, handleAction: React.PropTypes.func.isRequired,
@ -232,7 +285,9 @@ loop.contacts = (function(_, mozL10n) {
return ( return (
React.createElement("li", {className: contactCSSClass, onMouseLeave: this.hideDropdownMenu}, React.createElement("li", {className: contactCSSClass, onMouseLeave: this.hideDropdownMenu},
React.createElement("div", {className: "avatar"}), React.createElement("div", {className: "avatar"},
React.createElement("img", {src: navigator.mozLoop.getUserAvatar(email.value)})
),
React.createElement("div", {className: "details"}, React.createElement("div", {className: "details"},
React.createElement("div", {className: "username"}, React.createElement("strong", null, names.firstName), " ", names.lastName, React.createElement("div", {className: "username"}, React.createElement("strong", null, names.firstName), " ", names.lastName,
React.createElement("i", {className: cx({"icon icon-google": this.props.contact.category[0] == "google"})}), React.createElement("i", {className: cx({"icon icon-google": this.props.contact.category[0] == "google"})}),
@ -462,6 +517,12 @@ loop.contacts = (function(_, mozL10n) {
} }
}, },
handleUseGravatar: function() {
// We got permission to use Gravatar icons now, so we need to redraw the
// list entirely to show them.
this.refresh();
},
sortContacts: function(contact1, contact2) { sortContacts: function(contact1, contact2) {
let comp = contact1.name[0].localeCompare(contact2.name[0]); let comp = contact1.name[0].localeCompare(contact2.name[0]);
if (comp !== 0) { if (comp !== 0) {
@ -522,7 +583,8 @@ loop.contacts = (function(_, mozL10n) {
React.createElement("input", {className: "contact-filter", React.createElement("input", {className: "contact-filter",
placeholder: mozL10n.get("contacts_search_placesholder"), placeholder: mozL10n.get("contacts_search_placesholder"),
valueLink: this.linkState("filter")}) valueLink: this.linkState("filter")})
: null : null,
React.createElement(GravatarPromo, {handleUse: this.handleUseGravatar})
), ),
React.createElement("ul", {className: "contact-list"}, React.createElement("ul", {className: "contact-list"},
shownContacts.available ? shownContacts.available ?

View File

@ -81,6 +81,59 @@ loop.contacts = (function(_, mozL10n) {
contact[field][0].value = value; contact[field][0].value = value;
}; };
const GravatarPromo = React.createClass({
propTypes: {
handleUse: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
!navigator.mozLoop.getLoopPref("contacts.gravatars.show")
};
},
handleCloseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
this.setState({ showMe: false });
},
handleUseButtonClick: function() {
navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
this.setState({ showMe: false });
this.props.handleUse();
},
render: function() {
if (!this.state.showMe) {
return null;
}
let privacyUrl = navigator.mozLoop.getLoopPref("legal.privacy_url");
let message = mozL10n.get("gravatars_promo_message", {
"learn_more": React.renderToStaticMarkup(
<a href={privacyUrl} target="_blank">
{mozL10n.get("gravatars_promo_message_learnmore")}
</a>
)
});
return (
<div className="contacts-gravatar-promo">
<Button additionalClass="button-close" onClick={this.handleCloseButtonClick}/>
<p dangerouslySetInnerHTML={{__html: message}}></p>
<ButtonGroup>
<Button caption={mozL10n.get("gravatars_promo_button_nothanks")}
onClick={this.handleCloseButtonClick}/>
<Button caption={mozL10n.get("gravatars_promo_button_use")}
additionalClass="button-accept"
onClick={this.handleUseButtonClick}/>
</ButtonGroup>
</div>
);
}
});
const ContactDropdown = React.createClass({ const ContactDropdown = React.createClass({
propTypes: { propTypes: {
handleAction: React.PropTypes.func.isRequired, handleAction: React.PropTypes.func.isRequired,
@ -232,7 +285,9 @@ loop.contacts = (function(_, mozL10n) {
return ( return (
<li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}> <li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}>
<div className="avatar" /> <div className="avatar">
<img src={navigator.mozLoop.getUserAvatar(email.value)} />
</div>
<div className="details"> <div className="details">
<div className="username"><strong>{names.firstName}</strong> {names.lastName} <div className="username"><strong>{names.firstName}</strong> {names.lastName}
<i className={cx({"icon icon-google": this.props.contact.category[0] == "google"})} /> <i className={cx({"icon icon-google": this.props.contact.category[0] == "google"})} />
@ -462,6 +517,12 @@ loop.contacts = (function(_, mozL10n) {
} }
}, },
handleUseGravatar: function() {
// We got permission to use Gravatar icons now, so we need to redraw the
// list entirely to show them.
this.refresh();
},
sortContacts: function(contact1, contact2) { sortContacts: function(contact1, contact2) {
let comp = contact1.name[0].localeCompare(contact2.name[0]); let comp = contact1.name[0].localeCompare(contact2.name[0]);
if (comp !== 0) { if (comp !== 0) {
@ -523,6 +584,7 @@ loop.contacts = (function(_, mozL10n) {
placeholder={mozL10n.get("contacts_search_placesholder")} placeholder={mozL10n.get("contacts_search_placesholder")}
valueLink={this.linkState("filter")} /> valueLink={this.linkState("filter")} />
: null } : null }
<GravatarPromo handleUse={this.handleUseGravatar}/>
</div> </div>
<ul className="contact-list"> <ul className="contact-list">
{shownContacts.available ? {shownContacts.available ?

View File

@ -40,6 +40,18 @@ loop.panel = (function(_, mozL10n) {
if (tabChange) { if (tabChange) {
this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab); this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
} }
if (!tabChange && nextProps.buttonsHidden) {
if (nextProps.buttonsHidden.length !== this.props.buttonsHidden.length) {
tabChange = true;
} else {
for (var i = 0, l = nextProps.buttonsHidden.length; i < l && !tabChange; ++i) {
if (this.props.buttonsHidden.indexOf(nextProps.buttonsHidden[i]) === -1) {
tabChange = true;
}
}
}
}
return tabChange; return tabChange;
}, },

View File

@ -40,6 +40,18 @@ loop.panel = (function(_, mozL10n) {
if (tabChange) { if (tabChange) {
this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab); this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
} }
if (!tabChange && nextProps.buttonsHidden) {
if (nextProps.buttonsHidden.length !== this.props.buttonsHidden.length) {
tabChange = true;
} else {
for (var i = 0, l = nextProps.buttonsHidden.length; i < l && !tabChange; ++i) {
if (this.props.buttonsHidden.indexOf(nextProps.buttonsHidden[i]) === -1) {
tabChange = true;
}
}
}
}
return tabChange; return tabChange;
}, },

View File

@ -72,7 +72,11 @@ loop.Dispatcher = (function() {
} }
registeredStores.forEach(function(store) { registeredStores.forEach(function(store) {
store[type](action); try {
store[type](action);
} catch (x) {
console.error("[Dispatcher] Dispatching action caused an exception: ", x);
}
}); });
this._active = false; this._active = false;

View File

@ -14,9 +14,71 @@ describe("loop.contacts", function() {
var fakeAddContactButtonText = "Fake Add Contact"; var fakeAddContactButtonText = "Fake Add Contact";
var fakeEditContactButtonText = "Fake Edit Contact"; var fakeEditContactButtonText = "Fake Edit Contact";
var fakeDoneButtonText = "Fake Done"; var fakeDoneButtonText = "Fake Done";
// The fake contacts array is copied each time mozLoop.contacts.getAll() is called.
var fakeContacts = [{
id: 1,
_guid: 1,
name: ["Ally Avocado"],
email: [{
"pref": true,
"type": ["work"],
"value": "ally@mail.com"
}],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+31-6-12345678"
}],
category: ["google"],
published: 1406798311748,
updated: 1406798311748
},{
id: 2,
_guid: 2,
name: ["Bob Banana"],
email: [{
"pref": true,
"type": ["work"],
"value": "bob@gmail.com"
}],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+1-214-5551234"
}],
category: ["local"],
published: 1406798311748,
updated: 1406798311748
}, {
id: 3,
_guid: 3,
name: ["Caitlin Cantaloupe"],
email: [{
"pref": true,
"type": ["work"],
"value": "caitlin.cant@hotmail.com"
}],
category: ["local"],
published: 1406798311748,
updated: 1406798311748
}, {
id: 4,
_guid: 4,
name: ["Dave Dragonfruit"],
email: [{
"pref": true,
"type": ["work"],
"value": "dd@dragons.net"
}],
category: ["google"],
published: 1406798311748,
updated: 1406798311748
}];
var sandbox; var sandbox;
var fakeWindow; var fakeWindow;
var notifications; var notifications;
var listView;
var oldMozLoop = navigator.mozLoop;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
@ -32,6 +94,27 @@ describe("loop.contacts", function() {
} }
return JSON.stringify({textContent: textContentValue}); return JSON.stringify({textContent: textContentValue});
}, },
getLoopPref: function(pref) {
if (pref == "contacts.gravatars.promo") {
return true;
} else if (pref == "contacts.gravatars.show") {
return false;
}
return "";
},
setLoopPref: sandbox.stub(),
getUserAvatar: function() {
if (navigator.mozLoop.getLoopPref("contacts.gravatars.show")) {
return "gravatarsEnabled";
}
return "gravatarsDisabled";
},
contacts: {
getAll: function(callback) {
callback(null, [].concat(fakeContacts));
},
on: sandbox.stub()
}
}; };
fakeWindow = { fakeWindow = {
@ -39,18 +122,120 @@ describe("loop.contacts", function() {
}; };
loop.shared.mixins.setRootObject(fakeWindow); loop.shared.mixins.setRootObject(fakeWindow);
notifications = new loop.shared.models.NotificationCollection();
document.mozL10n.initialize(navigator.mozLoop); document.mozL10n.initialize(navigator.mozLoop);
}); });
afterEach(function() { afterEach(function() {
listView = null;
loop.shared.mixins.setRootObject(window); loop.shared.mixins.setRootObject(window);
navigator.mozLoop = oldMozLoop;
sandbox.restore(); sandbox.restore();
}); });
describe("GravatarsPromo", function() {
function checkGravatarContacts(enabled) {
var node = listView.getDOMNode();
// When gravatars are enabled, contacts should be rendered with gravatars.
var gravatars = node.querySelectorAll(".contact img[src=gravatarsEnabled]");
expect(gravatars.length).to.equal(enabled ? fakeContacts.length : 0);
// Sanity check the reverse:
gravatars = node.querySelectorAll(".contact img[src=gravatarsDisabled]");
expect(gravatars.length).to.equal(enabled ? 0 : fakeContacts.length);
}
it("should show the gravatars promo box", function() {
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
expect(promo).to.not.equal(null);
checkGravatarContacts(false);
});
it("should not show the gravatars promo box when the 'contacts.gravatars.promo' pref is set", function() {
navigator.mozLoop.getLoopPref = function(pref) {
if (pref == "contacts.gravatars.promo") {
return false;
} else if (pref == "contacts.gravatars.show") {
return true;
}
return "";
};
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
expect(promo).to.equal(null);
checkGravatarContacts(true);
});
it("should hide the gravatars promo box when the 'use' button is clicked", function() {
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
".contacts-gravatar-promo .button-accept"));
sinon.assert.calledTwice(navigator.mozLoop.setLoopPref);
var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
expect(promo).to.equal(null);
});
it("should should set the prefs correctly when the 'use' button is clicked", function() {
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
".contacts-gravatar-promo .button-accept"));
sinon.assert.calledTwice(navigator.mozLoop.setLoopPref);
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopPref, "contacts.gravatars.promo", false);
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopPref, "contacts.gravatars.show", true);
});
it("should hide the gravatars promo box when the 'close' button is clicked", function() {
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
".contacts-gravatar-promo .button-close"));
var promo = listView.getDOMNode().querySelector(".contacts-gravatar-promo");
expect(promo).to.equal(null);
});
it("should set prefs correctly when the 'close' button is clicked", function() {
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
React.addons.TestUtils.Simulate.click(listView.getDOMNode().querySelector(
".contacts-gravatar-promo .button-close"));
sinon.assert.calledOnce(navigator.mozLoop.setLoopPref);
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopPref,
"contacts.gravatars.promo", false);
});
});
describe("ContactsList", function () { describe("ContactsList", function () {
var listView;
beforeEach(function() { beforeEach(function() {
navigator.mozLoop.calls = { navigator.mozLoop.calls = {
startDirectCall: sandbox.stub(), startDirectCall: sandbox.stub(),
@ -58,19 +243,12 @@ describe("loop.contacts", function() {
}; };
navigator.mozLoop.contacts = {getAll: sandbox.stub()}; navigator.mozLoop.contacts = {getAll: sandbox.stub()};
notifications = new loop.shared.models.NotificationCollection();
listView = TestUtils.renderIntoDocument( listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList, { React.createElement(loop.contacts.ContactsList, {
notifications: notifications notifications: notifications
})); }));
}); });
afterEach(function() {
listView = null;
delete navigator.mozLoop.calls;
delete navigator.mozLoop.contacts;
});
describe("#handleContactAction", function() { describe("#handleContactAction", function() {
it("should call window.close when called with 'video-call' action", it("should call window.close when called with 'video-call' action",
function() { function() {

View File

@ -38,6 +38,8 @@ add_task(function* test_LoopUI_getters() {
yield loadLoopPanel(); yield loadLoopPanel();
Assert.ok(LoopUI.browser, "Browser element should be there"); Assert.ok(LoopUI.browser, "Browser element should be there");
Assert.strictEqual(LoopUI.selectedTab, "rooms", "Initially the rooms tab should be selected"); Assert.strictEqual(LoopUI.selectedTab, "rooms", "Initially the rooms tab should be selected");
let panelTabs = LoopUI.browser.contentDocument.querySelectorAll(".tab-view > li");
Assert.strictEqual(panelTabs.length, 1, "Only one tab, 'rooms', should be visible");
// Hide the panel. // Hide the panel.
yield LoopUI.togglePanel(); yield LoopUI.togglePanel();
@ -48,6 +50,12 @@ add_task(function* test_LoopUI_getters() {
MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile; MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
yield MozLoopServiceInternal.notifyStatusChanged("login"); yield MozLoopServiceInternal.notifyStatusChanged("login");
yield LoopUI.togglePanel();
Assert.strictEqual(LoopUI.selectedTab, "rooms", "Rooms tab should still be selected");
panelTabs = LoopUI.browser.contentDocument.querySelectorAll(".tab-view > li");
Assert.strictEqual(panelTabs.length, 2, "Two tabs should be visible");
yield LoopUI.togglePanel();
// Programmatically select the contacts tab. // Programmatically select the contacts tab.
yield LoopUI.togglePanel(null, "contacts"); yield LoopUI.togglePanel(null, "contacts");
Assert.strictEqual(LoopUI.selectedTab, "contacts", "Contacts tab should be selected now"); Assert.strictEqual(LoopUI.selectedTab, "contacts", "Contacts tab should be selected now");

View File

@ -104,6 +104,31 @@ describe("loop.Dispatcher", function () {
sinon.assert.calledOnce(getDataStore2.getWindowData); sinon.assert.calledOnce(getDataStore2.getWindowData);
}); });
describe("Error handling", function() {
beforeEach(function() {
sandbox.stub(console, "error");
});
it("should handle uncaught exceptions", function() {
getDataStore1.getWindowData.throws("Uncaught Error");
dispatcher.dispatch(getDataAction);
dispatcher.dispatch(cancelAction);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
sinon.assert.calledOnce(cancelStore1.cancelCall);
});
it("should log uncaught exceptions", function() {
getDataStore1.getWindowData.throws("Uncaught Error");
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(console.error);
});
});
describe("Queued actions", function() { describe("Queued actions", function() {
beforeEach(function() { beforeEach(function() {
// Restore the stub, so that we can easily add a function to be // Restore the stub, so that we can easily add a function to be

View File

@ -43,6 +43,66 @@ var fakeRooms = [
} }
]; ];
var fakeContacts = [{
id: 1,
_guid: 1,
name: ["Ally Avocado"],
email: [{
"pref": true,
"type": ["work"],
"value": "ally@mail.com"
}],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+31-6-12345678"
}],
category: ["google"],
published: 1406798311748,
updated: 1406798311748
},{
id: 2,
_guid: 2,
name: ["Bob Banana"],
email: [{
"pref": true,
"type": ["work"],
"value": "bob@gmail.com"
}],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+1-214-5551234"
}],
category: ["local"],
published: 1406798311748,
updated: 1406798311748
}, {
id: 3,
_guid: 3,
name: ["Caitlin Cantaloupe"],
email: [{
"pref": true,
"type": ["work"],
"value": "caitlin.cant@hotmail.com"
}],
category: ["local"],
published: 1406798311748,
updated: 1406798311748
}, {
id: 4,
_guid: 4,
name: ["Dave Dragonfruit"],
email: [{
"pref": true,
"type": ["work"],
"value": "dd@dragons.net"
}],
category: ["google"],
published: 1406798311748,
updated: 1406798311748
}];
/** /**
* Faking the mozLoop object which doesn't exist in regular web pages. * Faking the mozLoop object which doesn't exist in regular web pages.
* @type {Object} * @type {Object}
@ -54,21 +114,28 @@ navigator.mozLoop = {
switch(pref) { switch(pref) {
// Ensure we skip FTE completely. // Ensure we skip FTE completely.
case "gettingStarted.seen": case "gettingStarted.seen":
case "contacts.gravatars.promo":
return true; return true;
case "contacts.gravatars.show":
return false;
} }
}, },
setLoopPref: function(){}, setLoopPref: function(){},
releaseCallData: function() {}, releaseCallData: function() {},
copyString: function() {}, copyString: function() {},
getUserAvatar: function(emailAddress) {
return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ?
"0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40";
},
contacts: { contacts: {
getAll: function(callback) { getAll: function(callback) {
callback(null, []); callback(null, [].concat(fakeContacts));
}, },
on: function() {} on: function() {}
}, },
rooms: { rooms: {
getAll: function(version, callback) { getAll: function(version, callback) {
callback(null, fakeRooms); callback(null, [].concat(fakeRooms));
}, },
on: function() {} on: function() {}
}, },

View File

@ -10,11 +10,22 @@ function checkRLState() {
let sidebarBroadcaster = document.getElementById("readingListSidebar"); let sidebarBroadcaster = document.getElementById("readingListSidebar");
let sidebarMenuitem = document.getElementById("menu_readingListSidebar"); let sidebarMenuitem = document.getElementById("menu_readingListSidebar");
let bookmarksMenubarItem = document.getElementById("menu_readingList");
let bookmarksMenubarSeparator = document.getElementById("menu_readingListSeparator");
if (enabled) { if (enabled) {
Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true", Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true",
"Sidebar broadcaster should not be hidden"); "Sidebar broadcaster should not be hidden");
Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true", Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
"Sidebar menuitem should be visible"); "Sidebar menuitem should be visible");
// Currently disabled on OSX.
if (bookmarksMenubarItem) {
Assert.notEqual(bookmarksMenubarItem.getAttribute("hidden"), "true",
"RL bookmarks submenu in menubar should not be hidden");
Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
"RL bookmarks separator in menubar should be visible");
}
} else { } else {
Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true", Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true",
"Sidebar broadcaster should be hidden"); "Sidebar broadcaster should be hidden");
@ -22,6 +33,14 @@ function checkRLState() {
"Sidebar menuitem should be hidden"); "Sidebar menuitem should be hidden");
Assert.equal(ReadingListUI.isSidebarOpen, false, Assert.equal(ReadingListUI.isSidebarOpen, false,
"ReadingListUI should not think sidebar is open"); "ReadingListUI should not think sidebar is open");
// Currently disabled on OSX.
if (bookmarksMenubarItem) {
Assert.equal(bookmarksMenubarItem.getAttribute("hidden"), "true",
"RL bookmarks submenu in menubar should not be hidden");
Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
"RL bookmarks separator in menubar should be visible");
}
} }
if (!enabled) { if (!enabled) {

View File

@ -266,7 +266,10 @@ function mousedown (win, button) {
EventUtils.sendMouseEvent({ type: "mousedown" }, button, win); EventUtils.sendMouseEvent({ type: "mousedown" }, button, win);
} }
function* startRecording(panel, options={}) { function* startRecording(panel, options = {
waitForOverview: true,
waitForStateChanged: true
}) {
let win = panel.panelWin; let win = panel.panelWin;
let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_START_RECORDING); let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_START_RECORDING);
let willStart = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_WILL_START); let willStart = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_WILL_START);
@ -287,10 +290,14 @@ function* startRecording(panel, options={}) {
"The record button should be locked."); "The record button should be locked.");
yield willStart; yield willStart;
let stateChanged = once(win.PerformanceView, win.EVENTS.UI_STATE_CHANGED); let stateChanged = options.waitForStateChanged
? once(win.PerformanceView, win.EVENTS.UI_STATE_CHANGED)
: Promise.resolve();
yield hasStarted; yield hasStarted;
let overviewRendered = options.waitForOverview ? once(win.OverviewView, win.EVENTS.OVERVIEW_RENDERED) : Promise.resolve(); let overviewRendered = options.waitForOverview
? once(win.OverviewView, win.EVENTS.OVERVIEW_RENDERED)
: Promise.resolve();
yield stateChanged; yield stateChanged;
yield overviewRendered; yield overviewRendered;
@ -304,7 +311,10 @@ function* startRecording(panel, options={}) {
"The record button should not be locked."); "The record button should not be locked.");
} }
function* stopRecording(panel, options={}) { function* stopRecording(panel, options = {
waitForOverview: true,
waitForStateChanged: true
}) {
let win = panel.panelWin; let win = panel.panelWin;
let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_STOP_RECORDING); let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_STOP_RECORDING);
let willStop = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_WILL_STOP); let willStop = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_WILL_STOP);
@ -325,10 +335,14 @@ function* stopRecording(panel, options={}) {
"The record button should be locked."); "The record button should be locked.");
yield willStop; yield willStop;
let stateChanged = once(win.PerformanceView, win.EVENTS.UI_STATE_CHANGED); let stateChanged = options.waitForStateChanged
? once(win.PerformanceView, win.EVENTS.UI_STATE_CHANGED)
: Promise.resolve();
yield hasStopped; yield hasStopped;
let overviewRendered = options.waitForOverview ? once(win.OverviewView, win.EVENTS.OVERVIEW_RENDERED) : Promise.resolve(); let overviewRendered = options.waitForOverview
? once(win.OverviewView, win.EVENTS.OVERVIEW_RENDERED)
: Promise.resolve();
yield stateChanged; yield stateChanged;
yield overviewRendered; yield overviewRendered;

View File

@ -34,11 +34,13 @@ let JsFlameGraphView = Heritage.extend(DetailsSubview, {
/** /**
* Unbinds events. * Unbinds events.
*/ */
destroy: function () { destroy: Task.async(function* () {
DetailsSubview.destroy.call(this); DetailsSubview.destroy.call(this);
this.graph.off("selecting", this._onRangeChangeInGraph); this.graph.off("selecting", this._onRangeChangeInGraph);
},
yield this.graph.destroy();
}),
/** /**
* Method for handling all the set up for rendering a new flamegraph. * Method for handling all the set up for rendering a new flamegraph.

View File

@ -33,11 +33,13 @@ let MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
/** /**
* Unbinds events. * Unbinds events.
*/ */
destroy: function () { destroy: Task.async(function* () {
DetailsSubview.destroy.call(this); DetailsSubview.destroy.call(this);
this.graph.off("selecting", this._onRangeChangeInGraph); this.graph.off("selecting", this._onRangeChangeInGraph);
},
yield this.graph.destroy();
}),
/** /**
* Method for handling all the set up for rendering a new flamegraph. * Method for handling all the set up for rendering a new flamegraph.

View File

@ -54,15 +54,15 @@ let OverviewView = {
/** /**
* Unbinds events. * Unbinds events.
*/ */
destroy: function () { destroy: Task.async(function*() {
if (this.markersOverview) { if (this.markersOverview) {
this.markersOverview.destroy(); yield this.markersOverview.destroy();
} }
if (this.memoryOverview) { if (this.memoryOverview) {
this.memoryOverview.destroy(); yield this.memoryOverview.destroy();
} }
if (this.framerateGraph) { if (this.framerateGraph) {
this.framerateGraph.destroy(); yield this.framerateGraph.destroy();
} }
PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged); PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
@ -71,7 +71,7 @@ let OverviewView = {
PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop); PerformanceController.off(EVENTS.RECORDING_WILL_STOP, this._onRecordingWillStop);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped); PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected); PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
}, }),
/** /**
* Disabled in the event we're using a Timeline mock, so we'll have no * Disabled in the event we're using a Timeline mock, so we'll have no

View File

@ -26,7 +26,7 @@ function* performTest() {
testGraph(host, graph); testGraph(host, graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -23,7 +23,7 @@ function* performTest() {
yield graph.ready(); yield graph.ready();
testGraph(host, graph); testGraph(host, graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -29,7 +29,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -30,7 +30,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -25,7 +25,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -27,7 +27,7 @@ function* performTest() {
testGraph(host, graph); testGraph(host, graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -22,7 +22,7 @@ function* performTest() {
testDataAndRegions(graph); testDataAndRegions(graph);
testHighlights(graph); testHighlights(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -21,7 +21,7 @@ function* performTest() {
yield testSelection(graph); yield testSelection(graph);
yield testCursor(graph); yield testCursor(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -19,7 +19,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -21,7 +21,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -21,7 +21,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -20,7 +20,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -20,7 +20,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -20,7 +20,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -19,7 +19,7 @@ function* performTest() {
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -21,7 +21,7 @@ function* performTest() {
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -19,7 +19,7 @@ function* performTest() {
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -20,7 +20,7 @@ function* performTest() {
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -22,7 +22,7 @@ function* performTest() {
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -48,5 +48,5 @@ function* testGraph (parent, options) {
is(graph._avgGutterLine.hidden, options.avg === false, is(graph._avgGutterLine.hidden, options.avg === false,
`The avg gutter should ${options.avg === false ? "not " : ""}be shown`); `The avg gutter should ${options.avg === false ? "not " : ""}be shown`);
graph.destroy(); yield graph.destroy();
} }

View File

@ -27,7 +27,7 @@ function* performTest() {
is(refreshCount, 2, "The graph should've been refreshed 2 times."); is(refreshCount, 2, "The graph should've been refreshed 2 times.");
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -33,7 +33,7 @@ function* performTest() {
is(refreshCount, 0, "The graph shouldn't have been refreshed at all."); is(refreshCount, 0, "The graph shouldn't have been refreshed at all.");
is(refreshCancelledCount, 2, "The graph should've had 2 refresh attempts."); is(refreshCancelledCount, 2, "The graph should've had 2 refresh attempts.");
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -25,7 +25,7 @@ function* performTest() {
testGraph(graph); testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -29,7 +29,7 @@ function* performTest() {
yield graph.once("ready"); yield graph.once("ready");
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -35,8 +35,8 @@ function* performTest() {
testGraphs(graph1, graph2); testGraphs(graph1, graph2);
graph1.destroy(); yield graph1.destroy();
graph2.destroy(); yield graph2.destroy();
host.destroy(); host.destroy();
} }

View File

@ -23,7 +23,7 @@ function* performTest() {
yield graph.ready(); yield graph.ready();
testGraph(host, graph); testGraph(host, graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -19,7 +19,7 @@ function* performTest() {
yield testGraph(graph); yield testGraph(graph);
graph.destroy(); yield graph.destroy();
host.destroy(); host.destroy();
} }

View File

@ -26,6 +26,7 @@ const OVERVIEW_ROW_HEIGHT = 11; // px
const OVERVIEW_SELECTION_LINE_COLOR = "#666"; const OVERVIEW_SELECTION_LINE_COLOR = "#666";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555"; const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
@ -199,9 +200,18 @@ MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
_findOptimalTickInterval: function(dataScale) { _findOptimalTickInterval: function(dataScale) {
let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE; let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE;
let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio; let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > spacingMin) {
return dataScale;
}
while (true) { while (true) {
let scaledStep = dataScale * timingStep; let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < spacingMin) { if (scaledStep < spacingMin) {
timingStep <<= 1; timingStep <<= 1;
continue; continue;

View File

@ -27,6 +27,7 @@ const WATERFALL_SIDEBAR_WIDTH = 150; // px
const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30; const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
const WATERFALL_HEADER_TEXT_PADDING = 3; // px const WATERFALL_HEADER_TEXT_PADDING = 3; // px
@ -575,9 +576,18 @@ Waterfall.prototype = {
*/ */
_findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) { _findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) {
let timingStep = ticksMultiple; let timingStep = ticksMultiple;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > ticksSpacingMin) {
return dataScale;
}
while (true) { while (true) {
let scaledStep = dataScale * timingStep; let scaledStep = dataScale * timingStep;
if (++numIters > maxIters) {
return scaledStep;
}
if (scaledStep < ticksSpacingMin) { if (scaledStep < ticksSpacingMin) {
timingStep <<= 1; timingStep <<= 1;
continue; continue;

View File

@ -26,6 +26,7 @@ const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5; const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
const GRAPH_MIN_SELECTION_WIDTH = 0.001; // ms const GRAPH_MIN_SELECTION_WIDTH = 0.001; // ms
const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
const TIMELINE_TICKS_MULTIPLE = 5; // ms const TIMELINE_TICKS_MULTIPLE = 5; // ms
const TIMELINE_TICKS_SPACING_MIN = 75; // px const TIMELINE_TICKS_SPACING_MIN = 75; // px
@ -180,7 +181,9 @@ FlameGraph.prototype = {
/** /**
* Destroys this graph. * Destroys this graph.
*/ */
destroy: function() { destroy: Task.async(function*() {
yield this.ready();
this._window.removeEventListener("mousemove", this._onMouseMove); this._window.removeEventListener("mousemove", this._onMouseMove);
this._window.removeEventListener("mousedown", this._onMouseDown); this._window.removeEventListener("mousedown", this._onMouseDown);
this._window.removeEventListener("mouseup", this._onMouseUp); this._window.removeEventListener("mouseup", this._onMouseUp);
@ -200,7 +203,7 @@ FlameGraph.prototype = {
this._data = null; this._data = null;
this.emit("destroyed"); this.emit("destroyed");
}, }),
/** /**
* Rendering options. Subclasses should override these. * Rendering options. Subclasses should override these.
@ -789,12 +792,17 @@ FlameGraph.prototype = {
_findOptimalTickInterval: function(dataScale) { _findOptimalTickInterval: function(dataScale) {
let timingStep = TIMELINE_TICKS_MULTIPLE; let timingStep = TIMELINE_TICKS_MULTIPLE;
let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio; let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio;
let maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
let numIters = 0;
if (dataScale > spacingMin) { if (dataScale > spacingMin) {
return dataScale; return dataScale;
} }
while (true) { while (true) {
if (++numIters > maxIters) {
return scaledStep;
}
let scaledStep = dataScale * timingStep; let scaledStep = dataScale * timingStep;
if (scaledStep < spacingMin) { if (scaledStep < spacingMin) {
timingStep <<= 1; timingStep <<= 1;

View File

@ -229,7 +229,9 @@ AbstractCanvasGraph.prototype = {
/** /**
* Destroys this graph. * Destroys this graph.
*/ */
destroy: function() { destroy: Task.async(function *() {
yield this.ready();
this._window.removeEventListener("mousemove", this._onMouseMove); this._window.removeEventListener("mousemove", this._onMouseMove);
this._window.removeEventListener("mousedown", this._onMouseDown); this._window.removeEventListener("mousedown", this._onMouseDown);
this._window.removeEventListener("mouseup", this._onMouseUp); this._window.removeEventListener("mouseup", this._onMouseUp);
@ -259,7 +261,7 @@ AbstractCanvasGraph.prototype = {
gCachedStripePattern.clear(); gCachedStripePattern.clear();
this.emit("destroyed"); this.emit("destroyed");
}, }),
/** /**
* Rendering options. Subclasses should override these. * Rendering options. Subclasses should override these.

View File

@ -387,9 +387,7 @@ PreviewController.prototype = {
this.onTabPaint(r); this.onTabPaint(r);
} }
} }
let preview = this.preview; this.preview.invalidate();
if (preview.visible)
preview.invalidate();
break; break;
case "TabAttrModified": case "TabAttrModified":
this.updateTitleAndTooltip(); this.updateTitleAndTooltip();

View File

@ -85,8 +85,7 @@ body {
} }
@media (min-resolution: 2dppx) { @media (min-resolution: 2dppx) {
#element-picker::before, #element-picker::before {
#toggle-all::before {
background-image: url("chrome://browser/skin/devtools/command-pick@2x.png"); background-image: url("chrome://browser/skin/devtools/command-pick@2x.png");
background-size: 64px; background-size: 64px;
} }

View File

@ -81,7 +81,7 @@ public class CustomEditText extends ThemedEditText {
super.setPrivateMode(isPrivate); super.setPrivateMode(isPrivate);
mHighlightColor = getContext().getResources().getColor(isPrivate mHighlightColor = getContext().getResources().getColor(isPrivate
? R.color.url_bar_text_highlight_pb : R.color.url_bar_text_highlight); ? R.color.url_bar_text_highlight_pb : R.color.fennec_ui_orange);
// android:textColorHighlight cannot support a ColorStateList. // android:textColorHighlight cannot support a ColorStateList.
setHighlightColor(mHighlightColor); setHighlightColor(mHighlightColor);
} }

View File

@ -2,7 +2,7 @@
<item android:id="@android:id/progress"> <item android:id="@android:id/progress">
<clip> <clip>
<shape> <shape>
<solid android:color="@color/highlight_orange"/> <solid android:color="@color/fennec_ui_orange"/>
</shape> </shape>
</clip> </clip>
</item> </item>

View File

@ -5,7 +5,7 @@
<shape> <shape>
<gradient android:angle="90" <gradient android:angle="90"
android:startColor="#E66000" android:startColor="#E66000"
android:endColor="#FF9500" android:endColor="@color/fennec_ui_orange"
android:type="linear"/> android:type="linear"/>
<corners android:radius="4dp"/> <corners android:radius="4dp"/>

View File

@ -39,7 +39,7 @@
gecko:state_recording="false"> gecko:state_recording="false">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#FFFF9500"/> <solid android:color="@color/fennec_ui_orange"/>
<corners android:radius="3dp"/> <corners android:radius="3dp"/>
</shape> </shape>

View File

@ -52,7 +52,7 @@
<View android:id="@+id/divider_doorhanger" <View android:id="@+id/divider_doorhanger"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="#FFFF9500" android:background="@color/fennec_ui_orange"
android:visibility="gone"/> android:visibility="gone"/>
</merge> </merge>

View File

@ -16,7 +16,7 @@
android:inputType="text" android:inputType="text"
android:paddingLeft="@dimen/find_in_page_text_padding_left" android:paddingLeft="@dimen/find_in_page_text_padding_left"
android:paddingRight="@dimen/find_in_page_text_padding_right" android:paddingRight="@dimen/find_in_page_text_padding_right"
android:textColorHighlight="@color/url_bar_text_highlight" android:textColorHighlight="@color/fennec_ui_orange"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:gravity="center_vertical|left"/> android:gravity="center_vertical|left"/>

View File

@ -22,7 +22,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:gravity="center_vertical" android:gravity="center_vertical"
android:background="@color/firstrun_tabstrip" android:background="@color/firstrun_tabstrip"
gecko:tabIndicatorColor="@color/text_color_highlight" gecko:tabIndicatorColor="@color/fennec_ui_orange"
android:textColor="@color/android:white"/> android:textColor="@color/android:white"/>
</org.mozilla.gecko.firstrun.FirstrunPager> </org.mozilla.gecko.firstrun.FirstrunPager>
</org.mozilla.gecko.firstrun.FirstrunPane> </org.mozilla.gecko.firstrun.FirstrunPane>

View File

@ -18,7 +18,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:gravity="center_vertical" android:gravity="center_vertical"
android:background="@color/background_light" android:background="@color/background_light"
gecko:tabIndicatorColor="@color/text_color_highlight" gecko:tabIndicatorColor="@color/fennec_ui_orange"
android:textAppearance="@style/TextAppearance.Widget.HomePagerTabStrip"/> android:textAppearance="@style/TextAppearance.Widget.HomePagerTabStrip"/>
</org.mozilla.gecko.home.HomePager> </org.mozilla.gecko.home.HomePager>

View File

@ -23,7 +23,7 @@
android:background="@drawable/url_bar_entry" android:background="@drawable/url_bar_entry"
android:textColor="@color/url_bar_title" android:textColor="@color/url_bar_title"
android:textColorHint="@color/url_bar_title_hint" android:textColorHint="@color/url_bar_title_hint"
android:textColorHighlight="@color/url_bar_text_highlight" android:textColorHighlight="@color/fennec_ui_orange"
android:textSelectHandle="@drawable/handle_middle" android:textSelectHandle="@drawable/handle_middle"
android:textSelectHandleLeft="@drawable/handle_start" android:textSelectHandleLeft="@drawable/handle_start"
android:textSelectHandleRight="@drawable/handle_end" android:textSelectHandleRight="@drawable/handle_end"

View File

@ -16,7 +16,7 @@
android:textSize="@dimen/query_text_size" android:textSize="@dimen/query_text_size"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" android:focusableInTouchMode="false"
android:textColorHighlight="@color/highlight_orange" android:textColorHighlight="@color/fennec_ui_orange"
android:textSelectHandle="@drawable/handle_middle" android:textSelectHandle="@drawable/handle_middle"
android:textSelectHandleLeft="@drawable/handle_start" android:textSelectHandleLeft="@drawable/handle_start"
android:textSelectHandleRight="@drawable/handle_end" /> android:textSelectHandleRight="@drawable/handle_end" />

View File

@ -4,6 +4,28 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources> <resources>
<!-- Fennec color palette (bug 1127517) -->
<color name="fennec_ui_orange">#FF9500</color>
<color name="action_orange">#E66000</color>
<color name="action_orange_pressed">#DC5600</color>
<color name="link_blue">#0096DD</color>
<color name="private_browsing_purple">#CF68FF</color>
<color name="placeholder_active_grey">#222222</color>
<color name="placeholder_grey">#777777</color>
<color name="private_toolbar_grey">#292C29</color>
<color name="text_and_tabs_tray_grey">#363B40</color>
<color name="tabs_tray_grey_pressed">#45494E</color>
<color name="toolbar_icon_grey">#5F6368</color>
<color name="tabs_tray_icon_grey">#AFB1B3</color>
<color name="disabled_grey">#BFBFBF</color>
<color name="toolbar_grey_pressed">#D7D7DC</color>
<color name="toolbar_menu_dark_grey">#E1E1E6</color>
<color name="toolbar_grey">#EBEBF0</color>
<color name="about_page_header_grey">#F5F5F5</color>
<!-- Non-palette colors -->
<color name="primary">#363B40</color> <color name="primary">#363B40</color>
<color name="background_light">#FFF5F5F5</color> <color name="background_light">#FFF5F5F5</color>
@ -83,7 +105,6 @@
<color name="text_color_hint_floating_focused">#33b5e5</color> <color name="text_color_hint_floating_focused">#33b5e5</color>
<!-- Highlight colors --> <!-- Highlight colors -->
<color name="text_color_highlight">#FF9500</color>
<color name="text_color_highlight_inverse">#D06BFF</color> <color name="text_color_highlight_inverse">#D06BFF</color>
<!-- Link colors --> <!-- Link colors -->
@ -103,7 +124,6 @@
<color name="doorhanger_background_dark">#FFDDE4EA</color> <color name="doorhanger_background_dark">#FFDDE4EA</color>
<color name="validation_message_text">#ffffff</color> <color name="validation_message_text">#ffffff</color>
<color name="url_bar_text_highlight">#FFFF9500</color>
<color name="url_bar_text_highlight_pb">#FFD06BFF</color> <color name="url_bar_text_highlight_pb">#FFD06BFF</color>
<color name="suggestion_primary">#dddddd</color> <color name="suggestion_primary">#dddddd</color>
<color name="suggestion_pressed">#bbbbbb</color> <color name="suggestion_pressed">#bbbbbb</color>

View File

@ -6,8 +6,6 @@
<color name="global_background_color">#EBEBF0</color> <color name="global_background_color">#EBEBF0</color>
<color name="highlight_orange">#FF9500</color>
<color name="edit_text_default">#AFB1B3</color> <color name="edit_text_default">#AFB1B3</color>
<!-- card colors --> <!-- card colors -->

View File

@ -143,7 +143,7 @@
</style> </style>
<style name="Widget.ReadingListRow.ReadTime"> <style name="Widget.ReadingListRow.ReadTime">
<item name="android:textColor">@color/text_color_highlight</item> <item name="android:textColor">@color/fennec_ui_orange</item>
</style> </style>
<style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title"> <style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title">
@ -292,7 +292,7 @@
--> -->
<style name="TextAppearance"> <style name="TextAppearance">
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textColorHighlight">@color/text_color_highlight</item> <item name="android:textColorHighlight">@color/fennec_ui_orange</item>
<item name="android:textColorHint">?android:attr/textColorHint</item> <item name="android:textColorHint">?android:attr/textColorHint</item>
<item name="android:textColorLink">?android:attr/textColorLink</item> <item name="android:textColorLink">?android:attr/textColorLink</item>
<item name="android:textSize">@dimen/menu_item_textsize</item> <item name="android:textSize">@dimen/menu_item_textsize</item>
@ -457,7 +457,7 @@
<item name="android:textAppearance">@style/TextAppearance.UrlBar.Title</item> <item name="android:textAppearance">@style/TextAppearance.UrlBar.Title</item>
<item name="android:textColor">@color/url_bar_title</item> <item name="android:textColor">@color/url_bar_title</item>
<item name="android:textColorHint">@color/url_bar_title_hint</item> <item name="android:textColorHint">@color/url_bar_title_hint</item>
<item name="android:textColorHighlight">@color/url_bar_text_highlight</item> <item name="android:textColorHighlight">@color/fennec_ui_orange</item>
<item name="android:textSelectHandle">@drawable/handle_middle</item> <item name="android:textSelectHandle">@drawable/handle_middle</item>
<item name="android:textSelectHandleLeft">@drawable/handle_start</item> <item name="android:textSelectHandleLeft">@drawable/handle_start</item>
<item name="android:textSelectHandleRight">@drawable/handle_end</item> <item name="android:textSelectHandleRight">@drawable/handle_end</item>

View File

@ -49,7 +49,7 @@
<item name="android:textColorHintInverse">@color/text_color_hint_inverse</item> <item name="android:textColorHintInverse">@color/text_color_hint_inverse</item>
<!-- Highlight colors --> <!-- Highlight colors -->
<item name="android:textColorHighlight">@color/text_color_highlight</item> <item name="android:textColorHighlight">@color/fennec_ui_orange</item>
<item name="android:textColorHighlightInverse">@color/text_color_highlight_inverse</item> <item name="android:textColorHighlightInverse">@color/text_color_highlight_inverse</item>
<!-- Link colors --> <!-- Link colors -->

View File

@ -184,7 +184,7 @@ TaskbarPreview::GetActive(bool *active) {
NS_IMETHODIMP NS_IMETHODIMP
TaskbarPreview::Invalidate() { TaskbarPreview::Invalidate() {
if (!mVisible) if (!mVisible)
return NS_ERROR_FAILURE; return NS_OK;
// DWM Composition is required for previews // DWM Composition is required for previews
if (!nsUXThemeData::CheckForCompositor()) if (!nsUXThemeData::CheckForCompositor())