Merge m-c to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2014-10-17 16:38:26 +02:00
commit 63f8f831ed
67 changed files with 960 additions and 251 deletions

View File

@ -28,7 +28,6 @@ html xul|scrollbar {
background-image: none !important;
border: 0px solid transparent !important;
pointer-events: none;
opacity: 1;
}
xul|scrollbar[orient="vertical"] {
@ -56,7 +55,6 @@ xul|scrollbar[orient="horizontal"] xul|thumb {
xul|scrollbar:not([active="true"]),
xul|scrollbar[disabled] {
opacity: 0;
transition: opacity 1s ease;
}
xul|scrollbarbutton {

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f6b7471c881ee689183d681658cf2ba3dfc5610"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f6b7471c881ee689183d681658cf2ba3dfc5610"/>

View File

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "63436aa17e7fa3ad521fdeffdc22b81c36e5d69b",
"revision": "934b8c3014a3e20dd5d90ecf95f4b6b704dddb1e",
"repo_path": "/integration/gaia-central"
}

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="9f6b7471c881ee689183d681658cf2ba3dfc5610"/>

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="abef62c0623e5504a97b4fd411e879a67b285b52"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="1daf2dadcd0d554c733661a4c0be1b82001e9da0"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -782,7 +782,15 @@ function gKeywordURIFixup({ target: browser, data: fixupInfo }) {
notification.persistence = 1;
};
gDNSService.asyncResolve(hostName, 0, onLookupComplete, Services.tm.mainThread);
try {
gDNSService.asyncResolve(hostName, 0, onLookupComplete, Services.tm.mainThread);
} catch (ex) {
// Do nothing if the URL is invalid (we don't want to show a notification in that case).
if (ex.result != Cr.NS_ERROR_UNKNOWN_HOST) {
// ... otherwise, report:
Cu.reportError(ex);
}
}
}
// Called when a docshell has attempted to load a page in an incorrect process.
@ -6503,7 +6511,7 @@ var gIdentityHandler = {
// Chrome URIs however get special treatment. Some chrome URIs are
// whitelisted to provide a positive security signal to the user.
let whitelist = /^about:(accounts|addons|app-manager|config|crashes|customizing|healthreport|home|newaddon|permissions|preferences|privatebrowsing|sessionrestore|support|welcomeback)/i;
let whitelist = /^about:(accounts|addons|app-manager|config|crashes|customizing|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)/i;
let isChromeUI = uri.schemeIs("about") && whitelist.test(uri.spec);
if (isChromeUI) {
this.setMode(this.IDENTITY_MODE_CHROMEUI);

View File

@ -57,6 +57,7 @@ support-files =
file_dom_notifications.html
file_double_close_tab.html
file_favicon_change.html
file_favicon_change_not_in_document.html
file_fullscreen-window-open.html
get_user_media.html
head.js
@ -324,6 +325,8 @@ skip-if = e10s
skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
[browser_favicon_change.js]
skip-if = e10s
[browser_favicon_change_not_in_document.js]
skip-if = e10s
[browser_findbarClose.js]
skip-if = e10s # Bug ?????? - test directly manipulates content (tries to grab an iframe directly from content)
[browser_fullscreen-window-open.js]

View File

@ -0,0 +1,34 @@
"use strict";
const TEST_URL = "http://mochi.test:8888/browser/browser/base/content/test/general/file_favicon_change_not_in_document.html"
add_task(function*() {
let extraTab = gBrowser.selectedTab = gBrowser.addTab();
let tabLoaded = promiseTabLoaded(extraTab);
extraTab.linkedBrowser.loadURI(TEST_URL);
let expectedFavicon = "http://example.org/one-icon";
let haveChanged = new Promise.defer();
let observer = new MutationObserver(function(mutations) {
for (let mut of mutations) {
if (mut.attributeName != "image") {
continue;
}
let imageVal = extraTab.getAttribute("image").replace(/#.*$/, "");
if (!imageVal) {
// The value gets removed because it doesn't load.
continue;
}
is(imageVal, expectedFavicon, "Favicon image should correspond to expected image.");
haveChanged.resolve();
}
});
observer.observe(extraTab, {attributes: true});
yield tabLoaded;
expectedFavicon = "http://example.org/yet-another-icon";
haveChanged = new Promise.defer();
yield haveChanged.promise;
observer.disconnect();
gBrowser.removeTab(extraTab);
});

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html><head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="icon" href="http://example.org/one-icon" type="image/ico" id="i">
</head>
<body onload="onload()">
<script>
function onload() {
var ico = document.createElement("link");
ico.setAttribute("rel", "icon");
ico.setAttribute("type", "image/ico");
ico.setAttribute("href", "http://example.org/other-icon");
setTimeout(function() {
ico.setAttribute("href", "http://example.org/yet-another-icon");
document.getElementById("i").remove();
document.head.appendChild(ico);
}, 1000);
}
</script>
</body></html>

View File

@ -601,15 +601,18 @@ function injectLoopAPI(targetWindow) {
/**
* Composes an email via the external protocol service.
*
* @param {String} subject Subject of the email to send
* @param {String} body Body message of the email to send
* @param {String} subject Subject of the email to send
* @param {String} body Body message of the email to send
* @param {String} recipient Recipient email address (optional)
*/
composeEmail: {
enumerable: true,
writable: true,
value: function(subject, body) {
let mailtoURL = "mailto:?subject=" + encodeURIComponent(subject) + "&" +
"body=" + encodeURIComponent(body);
value: function(subject, body, recipient) {
recipient = recipient || "";
let mailtoURL = "mailto:" + encodeURIComponent(recipient) +
"?subject=" + encodeURIComponent(subject) +
"&body=" + encodeURIComponent(body);
extProtocolSvc.loadURI(CommonUtils.makeURI(mailtoURL));
}
},

View File

@ -12,8 +12,20 @@ loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
function _getPreferredEmail(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length === 0) {
return { value: "" };
}
return contact.email.find(e => e.pref) || contact.email[0];
}
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
@ -93,17 +105,6 @@ loop.conversationViews = (function(mozL10n) {
contact: React.PropTypes.object
},
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
_getPreferredEmail: function(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length == 0) {
return { value: "" };
}
return contact.email.find(e => e.pref) || contact.email[0];
},
render: function() {
var contactName;
@ -111,7 +112,7 @@ loop.conversationViews = (function(mozL10n) {
this.props.contact.name[0]) {
contactName = this.props.contact.name[0];
} else {
contactName = this._getPreferredEmail(this.props.contact).value;
contactName = _getPreferredEmail(this.props.contact).value;
}
document.title = contactName;
@ -170,12 +171,10 @@ loop.conversationViews = (function(mozL10n) {
React.DOM.p({className: "btn-label"}, pendingStateString),
React.DOM.div({className: "btn-group call-action-group"},
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
React.DOM.button({className: btnCancelStyles,
onClick: this.cancelCall},
mozL10n.get("initiate_call_cancel_button")
),
React.DOM.div({className: "fx-embedded-call-button-spacer"})
React.DOM.button({className: btnCancelStyles,
onClick: this.cancelCall},
mozL10n.get("initiate_call_cancel_button")
)
)
)
@ -187,8 +186,54 @@ loop.conversationViews = (function(mozL10n) {
* Call failed view. Displayed when a call fails.
*/
var CallFailedView = React.createClass({displayName: 'CallFailedView',
mixins: [Backbone.Events],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
store: React.PropTypes.instanceOf(
loop.store.ConversationStore).isRequired,
contact: React.PropTypes.object.isRequired,
// This is used by the UI showcase.
emailLinkError: React.PropTypes.bool,
},
getInitialState: function() {
return {
emailLinkError: this.props.emailLinkError,
emailLinkButtonDisabled: false
};
},
componentDidMount: function() {
this.listenTo(this.props.store, "change:emailLink",
this._onEmailLinkReceived);
this.listenTo(this.props.store, "error:emailLink",
this._onEmailLinkError);
},
componentWillUnmount: function() {
this.stopListening(this.props.store);
},
_onEmailLinkReceived: function() {
var emailLink = this.props.store.get("emailLink");
var contactEmail = _getPreferredEmail(this.props.contact).value;
sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
window.close();
},
_onEmailLinkError: function() {
this.setState({
emailLinkError: true,
emailLinkButtonDisabled: false
});
},
_renderError: function() {
if (!this.state.emailLinkError) {
return;
}
return React.DOM.p({className: "error"}, mozL10n.get("unable_retrieve_url"));
},
retryCall: function() {
@ -199,25 +244,38 @@ loop.conversationViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
emailLink: function() {
this.setState({
emailLinkError: false,
emailLinkButtonDisabled: true
});
this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
},
render: function() {
return (
React.DOM.div({className: "call-window"},
React.DOM.h2(null, mozL10n.get("generic_failure_title")),
React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_no_reason2")),
React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")),
this._renderError(),
React.DOM.div({className: "btn-group call-action-group"},
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
React.DOM.button({className: "btn btn-accept btn-retry",
onClick: this.retryCall},
mozL10n.get("retry_call_button")
),
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
React.DOM.button({className: "btn btn-cancel",
onClick: this.cancelCall},
mozL10n.get("cancel_button")
),
React.DOM.div({className: "fx-embedded-call-button-spacer"})
React.DOM.button({className: "btn btn-cancel",
onClick: this.cancelCall},
mozL10n.get("cancel_button")
),
React.DOM.button({className: "btn btn-info btn-retry",
onClick: this.retryCall},
mozL10n.get("retry_call_button")
),
React.DOM.button({className: "btn btn-info btn-email",
onClick: this.emailLink,
disabled: this.state.emailLinkButtonDisabled},
mozL10n.get("share_button2")
)
)
)
);
@ -425,7 +483,9 @@ loop.conversationViews = (function(mozL10n) {
}
case CALL_STATES.TERMINATED: {
return (CallFailedView({
dispatcher: this.props.dispatcher}
dispatcher: this.props.dispatcher,
store: this.props.store,
contact: this.state.contact}
));
}
case CALL_STATES.ONGOING: {
@ -445,7 +505,7 @@ loop.conversationViews = (function(mozL10n) {
callState: this.state.callState,
contact: this.state.contact,
enableCancelButton: this._isCancellable()}
))
));
}
}
},

View File

@ -12,8 +12,20 @@ loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
function _getPreferredEmail(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length === 0) {
return { value: "" };
}
return contact.email.find(e => e.pref) || contact.email[0];
}
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
@ -93,17 +105,6 @@ loop.conversationViews = (function(mozL10n) {
contact: React.PropTypes.object
},
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
_getPreferredEmail: function(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length == 0) {
return { value: "" };
}
return contact.email.find(e => e.pref) || contact.email[0];
},
render: function() {
var contactName;
@ -111,7 +112,7 @@ loop.conversationViews = (function(mozL10n) {
this.props.contact.name[0]) {
contactName = this.props.contact.name[0];
} else {
contactName = this._getPreferredEmail(this.props.contact).value;
contactName = _getPreferredEmail(this.props.contact).value;
}
document.title = contactName;
@ -170,12 +171,10 @@ loop.conversationViews = (function(mozL10n) {
<p className="btn-label">{pendingStateString}</p>
<div className="btn-group call-action-group">
<div className="fx-embedded-call-button-spacer"></div>
<button className={btnCancelStyles}
onClick={this.cancelCall}>
{mozL10n.get("initiate_call_cancel_button")}
</button>
<div className="fx-embedded-call-button-spacer"></div>
<button className={btnCancelStyles}
onClick={this.cancelCall}>
{mozL10n.get("initiate_call_cancel_button")}
</button>
</div>
</ConversationDetailView>
@ -187,8 +186,54 @@ loop.conversationViews = (function(mozL10n) {
* Call failed view. Displayed when a call fails.
*/
var CallFailedView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
store: React.PropTypes.instanceOf(
loop.store.ConversationStore).isRequired,
contact: React.PropTypes.object.isRequired,
// This is used by the UI showcase.
emailLinkError: React.PropTypes.bool,
},
getInitialState: function() {
return {
emailLinkError: this.props.emailLinkError,
emailLinkButtonDisabled: false
};
},
componentDidMount: function() {
this.listenTo(this.props.store, "change:emailLink",
this._onEmailLinkReceived);
this.listenTo(this.props.store, "error:emailLink",
this._onEmailLinkError);
},
componentWillUnmount: function() {
this.stopListening(this.props.store);
},
_onEmailLinkReceived: function() {
var emailLink = this.props.store.get("emailLink");
var contactEmail = _getPreferredEmail(this.props.contact).value;
sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
window.close();
},
_onEmailLinkError: function() {
this.setState({
emailLinkError: true,
emailLinkButtonDisabled: false
});
},
_renderError: function() {
if (!this.state.emailLinkError) {
return;
}
return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
},
retryCall: function() {
@ -199,25 +244,38 @@ loop.conversationViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
emailLink: function() {
this.setState({
emailLinkError: false,
emailLinkButtonDisabled: true
});
this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
},
render: function() {
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
<p className="btn-label">{mozL10n.get("generic_failure_no_reason2")}</p>
<p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>
{this._renderError()}
<div className="btn-group call-action-group">
<div className="fx-embedded-call-button-spacer"></div>
<button className="btn btn-accept btn-retry"
onClick={this.retryCall}>
{mozL10n.get("retry_call_button")}
</button>
<div className="fx-embedded-call-button-spacer"></div>
<button className="btn btn-cancel"
onClick={this.cancelCall}>
{mozL10n.get("cancel_button")}
</button>
<div className="fx-embedded-call-button-spacer"></div>
<button className="btn btn-cancel"
onClick={this.cancelCall}>
{mozL10n.get("cancel_button")}
</button>
<button className="btn btn-info btn-retry"
onClick={this.retryCall}>
{mozL10n.get("retry_call_button")}
</button>
<button className="btn btn-info btn-email"
onClick={this.emailLink}
disabled={this.state.emailLinkButtonDisabled}>
{mozL10n.get("share_button2")}
</button>
</div>
</div>
);
@ -426,6 +484,8 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.TERMINATED: {
return (<CallFailedView
dispatcher={this.props.dispatcher}
store={this.props.store}
contact={this.state.contact}
/>);
}
case CALL_STATES.ONGOING: {
@ -445,7 +505,7 @@ loop.conversationViews = (function(mozL10n) {
callState={this.state.callState}
contact={this.state.contact}
enableCancelButton={this._isCancellable()}
/>)
/>);
}
}
},

View File

@ -15,6 +15,7 @@ loop.panel = (function(_, mozL10n) {
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
@ -362,11 +363,7 @@ loop.panel = (function(_, mozL10n) {
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
navigator.mozLoop.composeEmail(
__("share_email_subject4", { clientShortname: __("clientShortname2")}),
__("share_email_body4", { callUrl: this.state.callUrl,
clientShortname: __("clientShortname2"),
learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
sharedUtils.composeCallUrlEmail(this.state.callUrl);
},
handleCopyButtonClick: function(event) {

View File

@ -15,6 +15,7 @@ loop.panel = (function(_, mozL10n) {
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
@ -362,11 +363,7 @@ loop.panel = (function(_, mozL10n) {
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
navigator.mozLoop.composeEmail(
__("share_email_subject4", { clientShortname: __("clientShortname2")}),
__("share_email_body4", { callUrl: this.state.callUrl,
clientShortname: __("clientShortname2"),
learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
sharedUtils.composeCallUrlEmail(this.state.callUrl);
},
handleCopyButtonClick: function(event) {

View File

@ -240,16 +240,27 @@
min-height: 230px;
}
.call-window > .btn-label {
text-align: center;
}
.call-window > .error {
text-align: center;
color: #f00;
font-size: 90%;
}
.call-action-group {
display: flex;
padding: 2.5em 0 0 0;
padding: 2.5em 4px 0 4px;
width: 100%;
justify-content: space-around;
}
.call-action-group > .btn {
margin-left: .5em;
height: 26px;
border-radius: 2px;
margin: 0 4px;
min-width: 64px;
}
.call-action-group .btn-group-chevron,

View File

@ -30,6 +30,13 @@ loop.shared.actions = (function() {
};
return {
/**
* Fetch a new call url from the server, intended to be sent over email when
* a contact can't be reached.
*/
FetchEmailLink: Action.define("fetchEmailLink", {
}),
/**
* Used to trigger gathering of initial call data.
*/

View File

@ -126,7 +126,8 @@ loop.store.ConversationStore = (function() {
"cancelCall",
"retryCall",
"mediaConnected",
"setMute"
"setMute",
"fetchEmailLink"
]);
},
@ -303,16 +304,47 @@ loop.store.ConversationStore = (function() {
this.set(muteType, !actionData.enabled);
},
/**
* Fetches a new call URL intended to be sent over email when a contact
* can't be reached.
*/
fetchEmailLink: function() {
// XXX This is an empty string as a conversation identifier. Bug 1015938 implements
// a user-set string.
this.client.requestCallUrl("", function(err, callUrlData) {
if (err) {
this.trigger("error:emailLink");
return;
}
this.set("emailLink", callUrlData.callUrl);
}.bind(this));
},
/**
* Obtains the outgoing call data from the server and handles the
* result.
*/
_setupOutgoingCall: function() {
var contactAddresses = [];
var contact = this.get("contact");
this.get("contact").email.forEach(function(address) {
contactAddresses.push(address.value);
});
function appendContactValues(property, strip) {
if (contact.hasOwnProperty(property)) {
contact[property].forEach(function(item) {
if (strip) {
contactAddresses.push(item.value
.replace(/^(\+)?(.*)$/g, function(m, prefix, number) {
return (prefix || "") + number.replace(/[\D]+/g, "");
}));
} else {
contactAddresses.push(item.value);
}
});
}
}
appendContactValues("email");
appendContactValues("tel", true);
this.client.setupOutgoingCall(contactAddresses,
this.get("callType"),

View File

@ -6,7 +6,7 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.utils = (function() {
loop.shared.utils = (function(mozL10n) {
"use strict";
/**
@ -96,11 +96,37 @@ loop.shared.utils = (function() {
}
};
/**
* Generates and opens a mailto: url with call URL information prefilled.
* Note: This only works for Desktop.
*
* @param {String} callUrl The call URL.
* @param {String} recipient The recipient email address (optional).
*/
function composeCallUrlEmail(callUrl, recipient) {
if (typeof navigator.mozLoop === "undefined") {
console.warn("composeCallUrlEmail isn't available for Loop standalone.");
return;
}
navigator.mozLoop.composeEmail(
mozL10n.get("share_email_subject4", {
clientShortname: mozL10n.get("clientShortname2")
}),
mozL10n.get("share_email_body4", {
callUrl: callUrl,
clientShortname: mozL10n.get("clientShortname2"),
learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl")
}),
recipient
);
}
return {
CALL_TYPES: CALL_TYPES,
Helper: Helper,
composeCallUrlEmail: composeCallUrlEmail,
formatDate: formatDate,
getTargetPlatform: getTargetPlatform,
getBoolPreference: getBoolPreference
};
})();
})(document.mozL10n || navigator.mozL10n);

View File

@ -6,6 +6,7 @@ var expect = chai.expect;
describe("loop.conversationViews", function () {
"use strict";
var sharedUtils = loop.shared.utils;
var sandbox, oldTitle, view, dispatcher, contact;
var CALL_STATES = loop.store.CALL_STATES;
@ -201,13 +202,25 @@ describe("loop.conversationViews", function () {
});
describe("CallFailedView", function() {
var store;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
loop.conversationViews.CallFailedView({
dispatcher: dispatcher
dispatcher: dispatcher,
store: store,
contact: {email: [{value: "test@test.tld"}]}
}));
}
beforeEach(function() {
store = new loop.store.ConversationStore({}, {
dispatcher: dispatcher,
client: {},
sdkDriver: {}
});
});
it("should dispatch a retryCall action when the retry button is pressed",
function() {
view = mountTestComponent();
@ -233,6 +246,66 @@ describe("loop.conversationViews", function () {
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
it("should dispatch a fetchEmailLink action when the cancel button is pressed",
function() {
view = mountTestComponent();
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
React.addons.TestUtils.Simulate.click(emailLinkBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "fetchEmailLink"));
});
it("should disable the email link button once the action is dispatched",
function() {
view = mountTestComponent();
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
React.addons.TestUtils.Simulate.click(emailLinkBtn);
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
});
it("should compose an email once the email link is received", function() {
var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
view = mountTestComponent();
store.set("emailLink", "http://fake.invalid/");
sinon.assert.calledOnce(composeCallUrlEmail);
sinon.assert.calledWithExactly(composeCallUrlEmail,
"http://fake.invalid/", "test@test.tld");
});
it("should close the conversation window once the email link is received",
function() {
sandbox.stub(window, "close");
view = mountTestComponent();
store.set("emailLink", "http://fake.invalid/");
sinon.assert.calledOnce(window.close);
});
it("should display an error message in case email link retrieval failed",
function() {
view = mountTestComponent();
store.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".error")).not.eql(null);
});
it("should allow retrying to get a call url if it failed previously",
function() {
view = mountTestComponent();
store.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
});
});
describe("OngoingConversationView", function() {

View File

@ -8,6 +8,7 @@
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
describe("loop.panel", function() {
"use strict";
@ -449,6 +450,7 @@ describe("loop.panel", function() {
it("should display a share button for email", function() {
fakeClient.requestCallUrl = sandbox.stub();
var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifications: notifications,
client: fakeClient
@ -457,7 +459,9 @@ describe("loop.panel", function() {
TestUtils.findRenderedDOMComponentWithClass(view, "button-email");
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
sinon.assert.calledOnce(navigator.mozLoop.composeEmail);
sinon.assert.calledOnce(composeCallUrlEmail);
sinon.assert.calledWithExactly(composeCallUrlEmail, "http://example.com");
});
it("should feature a copy button capable of copying the call url when clicked", function() {

View File

@ -38,7 +38,8 @@ describe("loop.store.ConversationStore", function () {
dispatcher = new loop.Dispatcher();
client = {
setupOutgoingCall: sinon.stub()
setupOutgoingCall: sinon.stub(),
requestCallUrl: sinon.stub()
};
sdkDriver = {
connectSession: sinon.stub(),
@ -275,6 +276,79 @@ describe("loop.store.ConversationStore", function () {
["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
it("should include all email addresses in the call data", function() {
contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
},
{
type: "work",
value: "emailFake",
pref: false
}]
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail", "emailFake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
it("should include trim phone numbers for the call data", function() {
contact = {
name: [ "Mr Smith" ],
tel: [{
type: "home",
value: "+44-5667+345 496(2335)45+ 456+",
pref: true
}]
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["+445667345496233545456"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
it("should include all email and telephone values in the call data", function() {
contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
}, {
type: "work",
value: "emailFake",
pref: false
}],
tel: [{
type: "work",
value: "01234567890",
pref: false
}, {
type: "home",
value: "09876543210",
pref: false
}]
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail", "emailFake", "01234567890", "09876543210"],
sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
describe("server response handling", function() {
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
@ -566,6 +640,37 @@ describe("loop.store.ConversationStore", function () {
});
});
describe("#fetchEmailLink", function() {
it("should request a new call url to the server", function() {
dispatcher.dispatch(new sharedActions.FetchEmailLink());
sinon.assert.calledOnce(client.requestCallUrl);
sinon.assert.calledWith(client.requestCallUrl, "");
});
it("should update the emailLink attribute when the new call url is received",
function() {
client.requestCallUrl = function(callId, cb) {
cb(null, {callUrl: "http://fake.invalid/"});
};
dispatcher.dispatch(new sharedActions.FetchEmailLink());
expect(store.get("emailLink")).eql("http://fake.invalid/");
});
it("should trigger an error:emailLink event in case of failure",
function() {
var trigger = sandbox.stub(store, "trigger");
client.requestCallUrl = function(callId, cb) {
cb("error");
};
dispatcher.dispatch(new sharedActions.FetchEmailLink());
sinon.assert.calledOnce(trigger);
sinon.assert.calledWithExactly(trigger, "error:emailLink");
});
});
describe("Events", function() {
describe("Websocket progress", function() {
beforeEach(function() {

View File

@ -18,6 +18,7 @@ describe("loop.shared.utils", function() {
});
afterEach(function() {
navigator.mozLoop = undefined;
sandbox.restore();
});
@ -110,7 +111,6 @@ describe("loop.shared.utils", function() {
describe("#getBoolPreference", function() {
afterEach(function() {
navigator.mozLoop = undefined;
localStorage.removeItem("test.true");
});
@ -142,4 +142,31 @@ describe("loop.shared.utils", function() {
});
});
});
describe("#composeCallUrlEmail", function() {
var composeEmail;
beforeEach(function() {
// fake mozL10n
sandbox.stub(navigator.mozL10n, "get", function(id) {
switch(id) {
case "share_email_subject4": return "subject";
case "share_email_body4": return "body";
}
});
composeEmail = sandbox.spy();
navigator.mozLoop = {
getLoopCharPref: sandbox.spy(),
composeEmail: composeEmail
};
});
it("should compose a call url email", function() {
sharedUtils.composeCallUrlEmail("http://invalid", "fake@invalid.tld");
sinon.assert.calledOnce(composeEmail);
sinon.assert.calledWith(composeEmail,
"subject", "body", "fake@invalid.tld");
});
});
});

View File

@ -312,6 +312,12 @@
React.DOM.div({className: "fx-embedded"},
CallFailedView({dispatcher: dispatcher})
)
),
Example({summary: "Call Failed — with call URL error", dashed: "true",
style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"},
CallFailedView({dispatcher: dispatcher, emailLinkError: true})
)
)
),

View File

@ -313,6 +313,12 @@
<CallFailedView dispatcher={dispatcher} />
</div>
</Example>
<Example summary="Call Failed — with call URL error" dashed="true"
style={{width: "260px", height: "265px"}}>
<div className="fx-embedded">
<CallFailedView dispatcher={dispatcher} emailLinkError={true} />
</div>
</Example>
</Section>
<Section name="StartConversationView">

View File

@ -41,7 +41,7 @@ FontInspector.prototype = {
*/
destroy: function FI_destroy() {
this.chromeDoc = null;
this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
this.inspector.sidebar.off("fontinspector-selected", this.onNewNode);
this.inspector.selection.off("new-node", this.onNewNode);
this.showAllButton.removeEventListener("click", this.showAll);
},

View File

@ -307,7 +307,9 @@ HTMLLinkElement::SetAttr(int32_t aNameSpaceID, nsIAtom* aName,
// to get updated information about the visitedness from Link.
if (aName == nsGkAtoms::href && kNameSpaceID_None == aNameSpaceID) {
Link::ResetLinkState(!!aNotify, true);
CreateAndDispatchEvent(OwnerDoc(), NS_LITERAL_STRING("DOMLinkChanged"));
if (IsInUncomposedDoc()) {
CreateAndDispatchEvent(OwnerDoc(), NS_LITERAL_STRING("DOMLinkChanged"));
}
}
if (NS_SUCCEEDED(rv) && aNameSpaceID == kNameSpaceID_None &&
@ -370,7 +372,9 @@ HTMLLinkElement::UnsetAttr(int32_t aNameSpaceID, nsIAtom* aAttribute,
// to get updated information about the visitedness from Link.
if (aAttribute == nsGkAtoms::href && kNameSpaceID_None == aNameSpaceID) {
Link::ResetLinkState(!!aNotify, false);
CreateAndDispatchEvent(OwnerDoc(), NS_LITERAL_STRING("DOMLinkChanged"));
if (IsInUncomposedDoc()) {
CreateAndDispatchEvent(OwnerDoc(), NS_LITERAL_STRING("DOMLinkChanged"));
}
}
return rv;

View File

@ -5,6 +5,8 @@
package org.mozilla.gecko;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONObject;
@ -19,12 +21,14 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import android.widget.TextView;
public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GeckoEventListener {
private static final String REQUEST_ID = "FindInPageBar";
private final Context mContext;
private CustomEditText mFindText;
private TextView mStatusText;
private boolean mInflated;
public FindInPageBar(Context context, AttributeSet attrs) {
@ -58,6 +62,8 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
}
});
mStatusText = (TextView) content.findViewById(R.id.find_status);
mInflated = true;
EventDispatcher.getInstance().registerGeckoThreadListener(this, "TextSelection:Data");
}
@ -71,6 +77,7 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
// handleMessage() receives response message and determines initial state of softInput
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Get", REQUEST_ID));
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Opened", null));
}
public void hide() {
@ -95,7 +102,7 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
@Override
public void afterTextChanged(Editable s) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Find", s.toString()));
sendRequestToFinderHelper("FindInPage:Find", s.toString());
}
@Override
@ -115,13 +122,13 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
final int viewId = v.getId();
if (viewId == R.id.find_prev) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Prev", mFindText.getText().toString()));
sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString());
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
return;
}
if (viewId == R.id.find_next) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Next", mFindText.getText().toString()));
sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString());
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
return;
}
@ -170,4 +177,28 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
});
}
}
/**
* Request find operation, and update matchCount results (current count and total).
*/
private void sendRequestToFinderHelper(String request, String searchString) {
GeckoAppShell.sendRequestToGecko(new GeckoRequest(request, searchString) {
@Override
public void onResponse(NativeJSObject nativeJSObject) {
final int total = nativeJSObject.optInt("total", 0);
final int current = nativeJSObject.optInt("current", 0);
final Boolean statusVisibility = (total > 0);
final String statusText = current + "/" + total;
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mStatusText.setVisibility(statusVisibility ? View.VISIBLE : View.GONE);
mStatusText.setText(statusText);
}
});
}
});
}
}

View File

@ -206,6 +206,13 @@ public class SendTab extends ShareMethod {
i++;
}
if (validGUIDs.isEmpty()) {
// Guess we'd better override. We have no clients.
// This does the broadcast for us.
setOverrideIntent(FxAccountGetStartedActivity.class);
return;
}
Intent uiStateIntent = getUIStateIntent();
uiStateIntent.putExtra(EXTRA_CLIENT_RECORDS, records);
broadcastUIState(uiStateIntent);
@ -230,6 +237,7 @@ public class SendTab extends ShareMethod {
Intent uiStateIntent = getUIStateIntent();
uiStateIntent.putExtra(OVERRIDE_INTENT, intent);
broadcastUIState(uiStateIntent);
}

View File

@ -112,24 +112,27 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
}
// The remaining states delegate to the SentTabTargetSelectedListener.
final String listenerGUID;
ParcelableClientRecord clientRecord = getItem(position);
final ParcelableClientRecord clientRecord = getItem(position);
if (currentState == State.LIST) {
row.setText(clientRecord.name);
row.setCompoundDrawablesWithIntrinsicBounds(getImage(clientRecord), 0, 0, 0);
listenerGUID = clientRecord.guid;
} else {
listenerGUID = null;
}
final String listenerGUID = clientRecord.guid;
row.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.onSendTabTargetSelected(listenerGUID);
}
});
row.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.onSendTabTargetSelected(listenerGUID);
}
});
} else {
row.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.onSendTabActionSelected();
}
});
}
return row;
}

View File

@ -6,7 +6,6 @@ package org.mozilla.gecko.overlays.ui;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LIST;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.NONE;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
import java.util.Arrays;
@ -111,16 +110,9 @@ public class SendTabList extends ListView {
public void setSyncClients(final ParcelableClientRecord[] c) {
final ParcelableClientRecord[] clients = c == null ? new ParcelableClientRecord[0] : c;
int size = clients.length;
if (size == 0) {
// Just show a button to set up Sync (or whatever).
switchState(NONE);
return;
}
clientListAdapter.setClientRecordList(Arrays.asList(clients));
if (size <= MAXIMUM_INLINE_ELEMENTS) {
if (clients.length <= MAXIMUM_INLINE_ELEMENTS) {
// Show the list of devices in-line.
switchState(LIST);
return;
@ -133,7 +125,7 @@ public class SendTabList extends ListView {
/**
* Get an AlertDialog listing all devices, allowing the user to select the one they want.
* Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all
* inline and looking crazy.
* inline and looking crazy).
*/
public AlertDialog getDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());

View File

@ -15,4 +15,11 @@ public interface SendTabTargetSelectedListener {
* @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null)
*/
public void onSendTabTargetSelected(String targetGUID);
/**
* Called when the overall Send Tab item is clicked.
*
* This implies that the clients list was unavailable.
*/
public void onSendTabActionSelected();
}

View File

@ -295,14 +295,17 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
* launching Fennec").
*/
public void sendTab(String targetGUID) {
// If an override intent has been set, dispatch it.
if (sendTabOverrideIntent != null) {
startActivity(sendTabOverrideIntent);
finish();
return;
}
@Override
public void onSendTabActionSelected() {
// This requires an override intent.
Assert.isTrue(sendTabOverrideIntent != null);
startActivity(sendTabOverrideIntent);
finish();
}
@Override
public void onSendTabTargetSelected(String targetGUID) {
// targetGUID being null with no override intent should be an impossible state.
Assert.isTrue(targetGUID != null);
@ -320,11 +323,6 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
slideOut();
}
@Override
public void onSendTabTargetSelected(String targetGUID) {
sendTab(targetGUID);
}
public void addToReadingList() {
startService(getServiceIntent(ShareMethod.Type.ADD_TO_READING_LIST));
slideOut();

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

View File

@ -6,34 +6,44 @@
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:layout_marginLeft="5dip"
android:layout_marginRight="5dip"
android:layout_marginLeft="@dimen/find_in_page_text_margin_left"
android:layout_marginRight="@dimen/find_in_page_text_margin_right"
android:contentDescription="@string/find_text"
android:background="@drawable/url_bar_entry"
android:singleLine="true"
android:textColor="#000000"
android:textCursorDrawable="@null"
android:inputType="text"
android:paddingLeft="15dip"
android:paddingRight="15dip"
android:paddingLeft="@dimen/find_in_page_text_padding_left"
android:paddingRight="@dimen/find_in_page_text_padding_right"
android:textColorHighlight="@color/url_bar_text_highlight"
android:imeOptions="actionSearch"
android:selectAllOnFocus="true"
android:gravity="center_vertical|left"/>
<TextView android:id="@+id/find_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/find_in_page_status_margin_right"
android:textColor="@color/find_status_default"
android:visibility="gone"/>
<ImageButton android:id="@+id/find_prev"
style="@style/FindBar.ImageButton"
android:contentDescription="@string/find_prev"
android:layout_marginTop="@dimen/find_in_page_control_margin_top"
android:src="@drawable/find_prev"/>
<ImageButton android:id="@+id/find_next"
style="@style/FindBar.ImageButton"
android:contentDescription="@string/find_next"
android:layout_marginTop="@dimen/find_in_page_control_margin_top"
android:src="@drawable/find_next"/>
<ImageButton android:id="@+id/find_close"
style="@style/FindBar.ImageButton"
android:contentDescription="@string/find_close"
android:layout_marginTop="@dimen/find_in_page_control_margin_top"
android:src="@drawable/find_close"/>
</merge>

View File

@ -0,0 +1,59 @@
<?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/. -->
<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/TabsItem"
android:focusable="true"
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="6dip"
android:paddingBottom="6dip"
android:paddingLeft="1dip"
android:paddingRight="1dip"
android:gravity="center"
android:orientation="vertical">
<LinearLayout android:layout_width="@dimen/new_tablet_tab_thumbnail_width"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="3dip">
<TextView android:id="@+id/title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:paddingTop="4dip"
style="@style/TabRowTextAppearance"
android:textSize="12sp"
android:textColor="#FFFFFFFF"
android:singleLine="true"
android:duplicateParentState="true"/>
<ImageButton android:id="@+id/close"
style="@style/TabsItemClose"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/action_bar_button_inverse"
android:scaleType="center"
android:contentDescription="@string/close_tab"
android:src="@drawable/new_tablet_tab_close"/>
</LinearLayout>
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="3dp"
android:background="@drawable/tab_thumbnail"
android:duplicateParentState="true">
<org.mozilla.gecko.widget.ThumbnailView android:id="@+id/thumbnail"
android:layout_width="@dimen/new_tablet_tab_thumbnail_width"
android:layout_height="@dimen/new_tablet_tab_thumbnail_height"
android:src="@drawable/tab_thumbnail_default"/>
</RelativeLayout>
</org.mozilla.gecko.tabs.TabsLayoutItemView>

View File

@ -8,4 +8,5 @@
<!-- Remote Tabs static view top padding. Less in landscape on phones. -->
<dimen name="home_remote_tabs_top_padding">16dp</dimen>
<dimen name="new_tablet_tab_panel_grid_padding">48dp</dimen>
</resources>

View File

@ -137,4 +137,8 @@
<color name="toast_button_background">#00000000</color>
<color name="toast_button_pressed">#DD2C3136</color>
<color name="toast_button_text">#FFFFFFFF</color>
<!-- Colour used for Find-In-Page dialog -->
<color name="find_status_default">#AFB1B3</color>
</resources>

View File

@ -118,6 +118,11 @@
<dimen name="url_bar_offset_left">32dp</dimen>
<dimen name="history_tab_indicator_height">50dp</dimen>
<dimen name="new_tablet_tab_thumbnail_height">180dp</dimen>
<dimen name="new_tablet_tab_thumbnail_width">180dp</dimen>
<dimen name="new_tablet_tab_panel_grid_padding">24dp</dimen>
<!-- PageActionButtons dimensions -->
<dimen name="page_action_button_width">32dp</dimen>
@ -144,4 +149,13 @@
<dimen name="arrow_popup_arrow_width">40dip</dimen>
<dimen name="arrow_popup_arrow_height">12dip</dimen>
<dimen name="arrow_popup_arrow_offset">8dp</dimen>
<!-- Find-In-Page dialog dimensions. -->
<dimen name="find_in_page_text_margin_left">5dip</dimen>
<dimen name="find_in_page_text_margin_right">12dip</dimen>
<dimen name="find_in_page_text_padding_left">10dip</dimen>
<dimen name="find_in_page_text_padding_right">10dip</dimen>
<dimen name="find_in_page_status_margin_right">5dip</dimen>
<dimen name="find_in_page_control_margin_top">2dip</dimen>
</resources>

View File

@ -16,8 +16,11 @@ import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
import org.mozilla.gecko.Tabs;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.GridView;
import android.view.ViewGroup;
@ -58,6 +61,18 @@ class TabsGridLayout extends GridView
item.setThumbnail(null);
}
});
setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
setStretchMode(GridView.STRETCH_SPACING);
setGravity(Gravity.CENTER);
setNumColumns(GridView.AUTO_FIT);
final Resources resources = getResources();
final int columnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_thumbnail_width);
setColumnWidth(columnWidth);
final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding);
setPadding(padding, 0, padding, 0);
}
private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
@ -66,7 +81,7 @@ class TabsGridLayout extends GridView
final private View.OnClickListener mSelectClickListener;
public TabsGridLayoutAdapter (Context context) {
super(context);
super(context, R.layout.new_tablet_tabs_item_cell);
mCloseClickListener = new Button.OnClickListener() {
@Override

View File

@ -21,12 +21,14 @@ public class TabsLayoutAdapter extends BaseAdapter {
public static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
private final Context mContext;
private final int mTabLayoutId;
private ArrayList<Tab> mTabs;
private final LayoutInflater mInflater;
public TabsLayoutAdapter (Context context) {
public TabsLayoutAdapter (Context context, int tabLayoutId) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
mTabLayoutId = tabLayoutId;
}
final void setTabs (ArrayList<Tab> tabs) {
@ -83,7 +85,7 @@ public class TabsLayoutAdapter extends BaseAdapter {
}
TabsLayoutItemView newView(int position, ViewGroup parent) {
return (TabsLayoutItemView) mInflater.inflate(R.layout.tabs_layout_item_view, parent, false);
return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false);
}
void bindView(TabsLayoutItemView view, Tab tab) {

View File

@ -89,7 +89,7 @@ class TabsListLayout extends TwoWayView
private class TabsListLayoutAdapter extends TabsLayoutAdapter {
private final Button.OnClickListener mCloseOnClickListener;
public TabsListLayoutAdapter (Context context) {
super(context);
super(context, R.layout.tabs_layout_item_view);
mCloseOnClickListener = new Button.OnClickListener() {
@Override

View File

@ -8,35 +8,100 @@ var FindHelper = {
_targetTab: null,
_initialViewport: null,
_viewportChanged: false,
_matchesCountResult: null,
observe: function(aMessage, aTopic, aData) {
switch(aTopic) {
case "FindInPage:Find":
this.doFind(aData);
case "FindInPage:Opened": {
this._findOpened();
this._init();
break;
}
case "FindInPage:Prev":
this.findAgain(aData, true);
case "Tab:Selected": {
// Allow for page switching.
this._uninit();
break;
}
case "FindInPage:Next":
this.findAgain(aData, false);
break;
case "Tab:Selected":
case "FindInPage:Closed":
this.findClosed();
this._uninit();
this._findClosed();
break;
}
},
_findOpened: function() {
Messaging.addListener((data) => {
this.doFind(data);
return this._getMatchesCountResult(data);
}, "FindInPage:Find");
Messaging.addListener((data) => {
this.findAgain(data, false);
return this._getMatchesCountResult(data);
}, "FindInPage:Next");
Messaging.addListener((data) => {
this.findAgain(data, true);
return this._getMatchesCountResult(data);
}, "FindInPage:Prev");
},
_init: function() {
// If there's no find in progress, start one.
if (this._finder) {
return;
}
this._targetTab = BrowserApp.selectedTab;
this._finder = this._targetTab.browser.finder;
this._finder.addResultListener(this);
this._initialViewport = JSON.stringify(this._targetTab.getViewport());
this._viewportChanged = false;
},
_uninit: function() {
// If there's no find in progress, there's nothing to clean up.
if (!this._finder) {
return;
}
this._finder.removeSelection();
this._finder.removeResultListener(this);
this._finder = null;
this._targetTab = null;
this._initialViewport = null;
this._viewportChanged = false;
},
_findClosed: function() {
Messaging.removeListener("FindInPage:Find");
Messaging.removeListener("FindInPage:Next");
Messaging.removeListener("FindInPage:Prev");
},
/**
* Request, wait for, and return the current matchesCount results for a string.
*/
_getMatchesCountResult: function(findString) {
// Sync call to Finder, results available immediately.
this._matchesCountResult = null;
this._finder.requestMatchesCount(findString);
return this._matchesCountResult;
},
/**
* Pass along the count results to FindInPageBar for display.
*/
onMatchesCountResult: function(result) {
this._matchesCountResult = result;
},
doFind: function(aSearchString) {
if (!this._finder) {
this._targetTab = BrowserApp.selectedTab;
this._finder = this._targetTab.browser.finder;
this._finder.addResultListener(this);
this._initialViewport = JSON.stringify(this._targetTab.getViewport());
this._viewportChanged = false;
this._init();
}
this._finder.fastFind(aSearchString, false);
@ -52,19 +117,6 @@ var FindHelper = {
this._finder.findAgain(aFindBackwards, false, false);
},
findClosed: function() {
// If there's no find in progress, there's nothing to clean up
if (!this._finder)
return;
this._finder.removeSelection();
this._finder.removeResultListener(this);
this._finder = null;
this._targetTab = null;
this._initialViewport = null;
this._viewportChanged = false;
},
onFindResult: function(aData) {
if (aData.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
if (this._viewportChanged) {

View File

@ -585,7 +585,6 @@ Readability.prototype = {
}
let topCandidate = topCandidates[0] || null;
let lastTopCandidate = (topCandidates.length > 3 ? topCandidates[topCandidates.length - 1] : null);
// If we still have no top candidate, just use the body as a last resort.
// We also have to copy the body node so it is something we can modify.
@ -704,17 +703,6 @@ Readability.prototype = {
return null;
}
} else {
if (lastTopCandidate !== null) {
// EXPERIMENTAL: Contrast ratio is how we measure the level of competition between candidates in the
// readability algorithm. This is to avoid offering reader mode on pages that are more like
// a list or directory of links with summaries. It takes the score of the last top candidate
// (see N_TOP_CANDIDATES) and checks how it compares to the top candidate's. On pages that are not
// actual articles, there will likely be many candidates with similar score (i.e. higher contrast ratio).
let contrastRatio = lastTopCandidate.readability.contentScore / topCandidate.readability.contentScore;
if (contrastRatio > 0.45)
return null;
}
return articleContent;
}
}

View File

@ -127,7 +127,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
#endif
["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],

View File

@ -1,7 +1,6 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"Assert_rejects",
"initializeIdentityWithTokenServerResponse",
];
@ -14,17 +13,6 @@ Cu.import("resource://services-common/tokenserverclient.js");
Cu.import("resource://testing-common/services/common/logging.js");
Cu.import("resource://testing-common/services/sync/utils.js");
// This shouldn't be here - it should be part of the xpcshell harness.
// Maybe as Assert.rejects - so we name it like that.
function Assert_rejects(promise, message) {
let deferred = Promise.defer();
promise.then(
() => deferred.reject(message || "Expected the promise to be rejected"),
deferred.resolve
);
return deferred.promise;
}
// Create a new browserid_identity object and initialize it with a
// mocked TokenServerClient which always receives the specified response.
this.initializeIdentityWithTokenServerResponse = function(response) {

View File

@ -269,12 +269,12 @@ add_task(function test_ensureLoggedIn() {
Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
"_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
Status.login = LOGIN_FAILED_NO_USERNAME;
yield Assert_rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user");
yield Assert.rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user");
Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
"_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
fxa.internal.currentAccountState.signedInUser = signedInUser;
Status.login = LOGIN_FAILED_LOGIN_REJECTED;
yield Assert_rejects(browseridManager.ensureLoggedIn(),
yield Assert.rejects(browseridManager.ensureLoggedIn(),
"LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
"status should remain LOGIN_FAILED_LOGIN_REJECTED");
@ -360,7 +360,7 @@ add_task(function test_getTokenErrors() {
let browseridManager = Service.identity;
yield browseridManager.initializeWithCurrentIdentity();
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to 401");
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
@ -376,7 +376,7 @@ add_task(function test_getTokenErrors() {
});
browseridManager = Service.identity;
yield browseridManager.initializeWithCurrentIdentity();
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to non-JSON response");
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
});
@ -397,7 +397,7 @@ add_task(function test_getTokenErrorWithRetry() {
let browseridManager = Service.identity;
yield browseridManager.initializeWithCurrentIdentity();
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to 503");
// The observer should have fired - check it got the value in the response.
@ -416,7 +416,7 @@ add_task(function test_getTokenErrorWithRetry() {
browseridManager = Service.identity;
yield browseridManager.initializeWithCurrentIdentity();
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to no token in response");
// The observer should have fired - check it got the value in the response.
@ -448,7 +448,7 @@ add_task(function test_getKeysErrorWithBackoff() {
});
let browseridManager = Service.identity;
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to 503");
// The observer should have fired - check it got the value in the response.
@ -482,7 +482,7 @@ add_task(function test_getKeysErrorWithRetry() {
});
let browseridManager = Service.identity;
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"should reject due to 503");
// The observer should have fired - check it got the value in the response.
@ -666,7 +666,7 @@ function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
browseridManager._fxaService = fxa;
browseridManager._signedInUser = null;
yield browseridManager.initializeWithCurrentIdentity();
yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
"expecting rejection due to hawk error");
}

View File

@ -17,7 +17,7 @@ add_task(function test_findCluster() {
});
yield Service.identity.initializeWithCurrentIdentity();
yield Assert_rejects(Service.identity.whenReadyToAuthenticate.promise,
yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
"should reject due to 500");
Assert.throws(function() {
@ -32,7 +32,7 @@ add_task(function test_findCluster() {
});
yield Service.identity.initializeWithCurrentIdentity();
yield Assert_rejects(Service.identity.whenReadyToAuthenticate.promise,
yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
"should reject due to 401");
cluster = Service._clusterManager._findCluster();

View File

@ -16,6 +16,10 @@ this.EXPORTED_SYMBOLS = [
"Assert"
];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
/**
* 1. The assert module provides functions that throw AssertionError's when
* particular conditions are not met.
@ -442,3 +446,34 @@ proto.throws = function(block, expected, message) {
this.report(false, expected, expected, message);
};
/**
* A promise that is expected to reject:
* assert.rejects(promise, expected, message);
*
* @param promise
* (promise) A promise that is expected to reject
* @param expected (optional)
* (mixed) Test reference to evaluate against the rejection result
* @param message (optional)
* (string) Short explanation of the expected result
*/
proto.rejects = function(promise, expected, message) {
return new Promise((resolve, reject) => {
if (typeof expected === "string") {
message = expected;
expected = null;
}
return promise.then(
() => this.report(true, null, expected, "Missing expected exception " + message),
err => {
if (expected && !expectedException(err, expected)) {
reject(err);
return;
}
this.report(false, err, expected, message);
resolve();
}
).then(null, reject);
});
};

View File

@ -297,4 +297,47 @@ function run_test() {
expected: "foo",
operator: "="
}).message, "[object Object] = \"foo\"");
run_next_test();
}
add_task(function* test_rejects() {
let ns = {};
Components.utils.import("resource://testing-common/Assert.jsm", ns);
let assert = new ns.Assert();
// A helper function to test failures.
function* checkRejectsFails(err, expected) {
try {
yield assert.rejects(Promise.reject(err), expected);
ok(false, "should have thrown");
} catch(ex) {
deepEqual(ex, err, "Assert.rejects threw the original unexpected error");
}
}
// A "throwable" error that's not an actual Error().
let SomeErrorLikeThing = function() {};
// The actual tests...
// No "expected" or "message" values supplied.
yield assert.rejects(Promise.reject(new Error("oh no")));
yield assert.rejects(Promise.reject("oh no"));
// An explicit error object:
// An instance to check against.
yield assert.rejects(Promise.reject(new Error("oh no")), Error, "rejected");
// A regex to match against the message.
yield assert.rejects(Promise.reject(new Error("oh no")), /oh no/, "rejected");
// Failure cases:
// An instance to check against that doesn't match.
yield checkRejectsFails(new Error("something else"), SomeErrorLikeThing);
// A regex that doesn't match.
yield checkRejectsFails(new Error("something else"), /oh no/);
// Check simple string messages.
yield assert.rejects(Promise.reject("oh no"), /oh no/, "rejected");
// Wrong message.
yield checkRejectsFails("something else", /oh no/);
});

View File

@ -401,7 +401,7 @@
}
.addon[active="false"] .icon {
filter: url("chrome://mozapps/skin/extensions/extensions.svg#greyscale");
filter: grayscale(1);
}
.addon-view[type="theme"] .icon {

View File

@ -146,7 +146,7 @@
.addon:not([active]) .addon-icon,
#disable-list .addon-icon,
#incompatible-list .addon-icon {
filter: url("chrome://mozapps/skin/extensions/extensions.svg#greyscale");
filter: grayscale(1);
}
#footer {

View File

@ -466,7 +466,7 @@
}
.addon[active="false"] .icon {
filter: url("chrome://mozapps/skin/extensions/extensions.svg#greyscale");
filter: grayscale(1);
}
.addon-view[type="theme"] .icon {

View File

@ -1,10 +0,0 @@
<?xml version="1.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/. -->
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="greyscale">
<feColorMatrix type="saturate" values="0"/>
</filter>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -145,7 +145,7 @@
.addon:not([active]) .addon-icon,
#disable-list .addon-icon,
#incompatible-list .addon-icon {
filter: url("chrome://mozapps/skin/extensions/extensions.svg#greyscale");
filter: grayscale(1);
}
#footer {

View File

@ -47,7 +47,6 @@ toolkit.jar:
skin/classic/mozapps/extensions/search.png (extensions/search.png)
skin/classic/mozapps/extensions/about.css (extensions/about.css)
* skin/classic/mozapps/extensions/extensions.css (extensions/extensions.css)
skin/classic/mozapps/extensions/extensions.svg (extensions/extensions.svg)
* skin/classic/mozapps/extensions/selectAddons.css (extensions/selectAddons.css)
skin/classic/mozapps/extensions/update.css (extensions/update.css)
skin/classic/mozapps/extensions/eula.css (extensions/eula.css)

View File

@ -473,7 +473,7 @@
}
.addon[active="false"] .icon {
filter: url("chrome://mozapps/skin/extensions/extensions.svg#greyscale");
filter: grayscale(1);
}

View File

@ -1,10 +0,0 @@
<?xml version="1.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/. -->
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="greyscale">
<feColorMatrix type="saturate" values="0"/>
</filter>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -158,7 +158,7 @@
.addon:not([active]) .addon-icon,
#disable-list .addon-icon,
#incompatible-list .addon-icon {
filter: url("chrome://mozapps/skin/extensions/extensions.svg#greyscale");
filter: grayscale(1);
}
#footer {

View File

@ -16,7 +16,6 @@ toolkit.jar:
* skin/classic/mozapps/extensions/extensions.css (extensions/extensions.css)
* skin/classic/mozapps/extensions/selectAddons.css (extensions/selectAddons.css)
skin/classic/mozapps/extensions/update.css (extensions/update.css)
skin/classic/mozapps/extensions/extensions.svg (extensions/extensions.svg)
skin/classic/mozapps/extensions/category-search.png (extensions/category-search.png)
skin/classic/mozapps/extensions/category-discover.png (extensions/category-discover.png)
skin/classic/mozapps/extensions/category-languages.png (extensions/localeGeneric.png)
@ -99,7 +98,6 @@ toolkit.jar:
* skin/classic/aero/mozapps/extensions/extensions.css (extensions/extensions-aero.css)
* skin/classic/aero/mozapps/extensions/selectAddons.css (extensions/selectAddons-aero.css)
skin/classic/aero/mozapps/extensions/update.css (extensions/update.css)
skin/classic/aero/mozapps/extensions/extensions.svg (extensions/extensions.svg)
skin/classic/aero/mozapps/extensions/category-search.png (extensions/category-search.png)
skin/classic/aero/mozapps/extensions/category-discover.png (extensions/category-discover-aero.png)
skin/classic/aero/mozapps/extensions/category-languages.png (extensions/localeGeneric-aero.png)