Merge m-c to b2g-inbound. a=merge

This commit is contained in:
Ryan VanderMeulen 2015-06-17 12:08:22 -04:00
commit 22f4c1f579
90 changed files with 2990 additions and 1556 deletions

View File

@ -60,7 +60,6 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
'browser.safebrowsing.enabled' : False,
'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
'browser.selfsupport.url': 'https://localhost/selfsupport-dummy',
'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',

View File

@ -17,7 +17,6 @@
"browser.safebrowsing.enabled": false,
"browser.safebrowsing.updateURL": "http://localhost/safebrowsing-dummy/update",
"browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
"browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
"browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
"browser.selfsupport.url": "https://localhost/selfsupport-dummy",
"browser.trackingprotection.gethashURL": "http://localhost/safebrowsing-dummy/gethash",

View File

@ -360,12 +360,9 @@ pref("browser.safebrowsing.malware.enabled", false);
pref("browser.safebrowsing.debug", false);
pref("browser.safebrowsing.updateURL", "https://safebrowsing.google.com/safebrowsing/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2&key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.gethashURL", "https://safebrowsing.google.com/safebrowsing/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
pref("browser.safebrowsing.reportURL", "https://safebrowsing.google.com/safebrowsing/report?");
pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportPhishMistakeURL", "https://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.reportPhishURL", "https://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.reportMalwareMistakeURL", "https://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.appRepURL", "https://sb-ssl.google.com/safebrowsing/clientreport/download?key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.id", "Firefox");

View File

@ -981,13 +981,9 @@ pref("browser.safebrowsing.debug", false);
pref("browser.safebrowsing.updateURL", "https://safebrowsing.google.com/safebrowsing/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2&key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.gethashURL", "https://safebrowsing.google.com/safebrowsing/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
pref("browser.safebrowsing.reportURL", "https://safebrowsing.google.com/safebrowsing/report?");
pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportPhishMistakeURL", "https://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.reportPhishURL", "https://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.reportMalwareMistakeURL", "https://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.malware.reportURL", "https://safebrowsing.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
pref("browser.safebrowsing.appRepURL", "https://sb-ssl.google.com/safebrowsing/clientreport/download?key=%GOOGLE_API_KEY%");

View File

@ -36,17 +36,7 @@ var gSafeBrowsing = {
* @return String the report phishing URL.
*/
getReportURL: function(name) {
var reportUrl = SafeBrowsing.getReportURL(name);
var pageUri = gBrowser.currentURI.clone();
// Remove the query to avoid including potentially sensitive data
if (pageUri instanceof Ci.nsIURL)
pageUri.query = '';
reportUrl += "&url=" + encodeURIComponent(pageUri.asciiSpec);
return reportUrl;
return SafeBrowsing.getReportURL(name, gBrowser.currentURI);
}
}
#endif

View File

@ -3019,7 +3019,7 @@ let BrowserOnClick = {
label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"),
accessKey: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.accessKey"),
callback: function() {
openUILinkIn(gSafeBrowsing.getReportURL('MalwareError'), 'tab');
openUILinkIn(gSafeBrowsing.getReportURL('MalwareMistake'), 'tab');
}
};
} else if (reason === 'phishing') {
@ -3028,7 +3028,7 @@ let BrowserOnClick = {
label: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.label"),
accessKey: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.accessKey"),
callback: function() {
openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');
openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');
}
};
} else if (reason === 'unwanted') {

View File

@ -29,7 +29,7 @@
accesskey="&reportPhishSiteMenu.accesskey;"
insertbefore="aboutSeparator"
observes="reportPhishingErrorBroadcaster"
oncommand="openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');"
oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');"
onclick="checkForMiddleClick(this, event);"/>
</menupopup>
</overlay>

View File

@ -58,15 +58,23 @@ function testBenignPage(gTestBrowser)
is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was ON and tracking was NOT present");
}
function testTrackingPage(gTestBrowser)
function* testTrackingPage(gTestBrowser)
{
// Make sure the doorhanger appears
var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
isnot(notification, null, "Tracking Content Doorhanger did appear when protection was ON and tracking was present");
notification.reshow();
// Wait for the method to be attached after showing the popup
yield promiseWaitForCondition(() => {
return PopupNotifications.panel.firstChild.disableTrackingContentProtection;
});
// Make sure the state of the doorhanger includes blocking tracking elements
isnot(PopupNotifications.panel.firstChild.isTrackingContentBlocked, 0,
"Tracking Content is being blocked");
is(PopupNotifications.panel.firstChild.isTrackingContentBlocked,
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
"Tracking Content is being blocked");
// Make sure the notification has no trackingblockdisabled attribute
ok(!PopupNotifications.panel.firstChild.hasAttribute("trackingblockdisabled"),
@ -127,7 +135,7 @@ add_task(function* () {
// Point tab to a test page containing tracking elements
yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html");
testTrackingPage(gBrowser.getBrowserForTab(tab));
yield testTrackingPage(gBrowser.getBrowserForTab(tab));
// Wait for tab to reload following tracking-protection page white-listing
yield promiseTabLoadEvent(tab);

View File

@ -247,40 +247,6 @@ body {
font-weight: 700;
}
.new-room-view > .context > .context-content {
border: 1px solid #0096dd;
border-radius: 3px;
background: #fff;
padding: .8em;
display: flex;
flex-flow: row nowrap;
line-height: 1.1em;
}
.new-room-view > .context > .context-content > .context-preview {
float: left;
width: 16px;
max-height: 16px;
-moz-margin-end: .8em;
flex: 0 1 auto;
}
html[dir="rtl"] .new-room-view > .context > .context-content > .context-preview {
float: left;
}
.new-room-view > .context > .context-content > .context-description {
flex: 0 1 auto;
display: block;
}
.new-room-view > .context > .context-content > .context-description > .context-url {
display: block;
color: #59A1D7;
font-weight: 700;
clear: both;
}
.new-room-view > .btn {
display: block;
font-size: 1rem;

View File

@ -765,20 +765,19 @@ loop.panel = (function(_, mozL10n) {
hide: !hostname ||
!this.props.mozLoop.getLoopPref("contextInConversations.enabled")
});
var thumbnail = this.state.previewImage || "loop/shared/img/icons-16x16.svg#globe";
return (
React.createElement("div", {className: "new-room-view"},
React.createElement("div", {className: contextClasses},
React.createElement(Checkbox, {label: mozL10n.get("context_inroom_label"),
onChange: this.onCheckboxChange}),
React.createElement("div", {className: "context-content"},
React.createElement("img", {className: "context-preview", src: thumbnail}),
React.createElement("span", {className: "context-description"},
this.state.description,
React.createElement("span", {className: "context-url"}, hostname)
)
)
React.createElement(sharedViews.ContextUrlView, {
allowClick: false,
description: this.state.description,
showContextTitle: false,
thumbnail: this.state.previewImage,
url: this.state.url,
useDesktopPaths: true})
),
React.createElement("button", {className: "btn btn-info new-room-button",
onClick: this.handleCreateButtonClick,

View File

@ -765,20 +765,19 @@ loop.panel = (function(_, mozL10n) {
hide: !hostname ||
!this.props.mozLoop.getLoopPref("contextInConversations.enabled")
});
var thumbnail = this.state.previewImage || "loop/shared/img/icons-16x16.svg#globe";
return (
<div className="new-room-view">
<div className={contextClasses}>
<Checkbox label={mozL10n.get("context_inroom_label")}
onChange={this.onCheckboxChange} />
<div className="context-content">
<img className="context-preview" src={thumbnail} />
<span className="context-description">
{this.state.description}
<span className="context-url">{hostname}</span>
</span>
</div>
<sharedViews.ContextUrlView
allowClick={false}
description={this.state.description}
showContextTitle={false}
thumbnail={this.state.previewImage}
url={this.state.url}
useDesktopPaths={true} />
</div>
<button className="btn btn-info new-room-button"
onClick={this.handleCreateButtonClick}

View File

@ -761,7 +761,10 @@ loop.roomViews = (function(mozL10n) {
mozLoop: this.props.mozLoop,
roomData: roomData,
show: !shouldRenderInvitationOverlay && shouldRenderContextView}),
React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher})
React.createElement(sharedViews.TextChatView, {
dispatcher: this.props.dispatcher,
showAlways: false,
showRoomName: false})
)
);
}

View File

@ -761,7 +761,10 @@ loop.roomViews = (function(mozL10n) {
mozLoop={this.props.mozLoop}
roomData={roomData}
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
<sharedViews.TextChatView dispatcher={this.props.dispatcher} />
<sharedViews.TextChatView
dispatcher={this.props.dispatcher}
showAlways={false}
showRoomName={false} />
</div>
);
}

View File

@ -501,3 +501,59 @@ html[dir="rtl"] .checkbox {
.checkbox.checked.disabled {
background-image: url("../img/check.svg#check-disabled");
}
/* ContextUrlView classes */
.context-content {
color: black;
text-align: left;
}
html[dir="rtl"] .context-content {
text-align: right;
}
.context-content > p {
font-weight: bold;
margin-bottom: .8em;
margin-top: 0;
}
.context-wrapper {
border: 1px solid #0096dd;
border-radius: 3px;
background: #fff;
padding: .8em;
/* Use the flex row mode to position the elements next to each other. */
display: flex;
flex-flow: row nowrap;
line-height: 1.1em;
}
.context-wrapper > .context-preview {
float: left;
/* 16px is standard height/width for a favicon */
width: 16px;
max-height: 16px;
margin-right: .8em;
flex: 0 1 auto;
}
html[dir="rtl"] .context-wrapper > .context-preview {
float: left;
margin-left: .8em;
margin-right: 0;
}
.context-wrapper > .context-description {
flex: 0 1 auto;
display: block;
color: black;
}
.context-wrapper > .context-description > .context-url {
display: block;
color: #59A1D7;
font-weight: 700;
clear: both;
}

View File

@ -266,11 +266,6 @@
z-index: 1001;
}
.standalone .room-conversation .local-stream,
.standalone .room-conversation .remote-inset-stream {
box-shadow: none;
}
/* Side by side video elements */
.conversation .media.side-by-side .focus-stream {
@ -715,7 +710,6 @@ html, .fx-embedded, #main,
min-width: 120px;
min-height: 150px;
width: 100%;
box-shadow: none;
}
/* Nested video elements */
@ -887,6 +881,13 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
background-image: url("../img/icons-16x16.svg#add-active");
}
.context-url-view-wrapper {
padding-left: 1em;
padding-right: 1em;
padding-bottom: 0.5em;
background-color: #E8F6FE;
}
.room-context {
background: rgba(0,0,0,.6);
border-top: 2px solid #444;
@ -1258,22 +1259,39 @@ html[dir="rtl"] .room-context-btn-edit {
margin-bottom: 1.5em;
}
.text-chat-entry > span {
.text-chat-entry > p {
border-width: 1px;
border-style: solid;
border-color: #0095dd;
border-radius: 10000px;
padding: .5em 1em;
/* Drop the default margins from the 'p' element. */
margin: 0;
/* inline-block stops the elements taking 100% of the text-chat-view width */
display: inline-block;
}
.text-chat-entry.received {
text-align: start;
}
.text-chat-entry.received > span {
.text-chat-entry.received > p {
border-color: #d8d8d8;
}
.text-chat-entry.special > p {
border: none;
}
.text-chat-entry.special.room-name {
color: black;
font-weight: bold;
text-align: start;
background-color: #E8F6FE;
padding-bottom: 0;
margin-bottom: 0;
}
.text-chat-box {
margin: auto;
}

View File

@ -5,18 +5,21 @@
var loop = loop || {};
loop.store = loop.store || {};
loop.store.TextChatStore = (function() {
loop.store.TextChatStore = (function(mozL10n) {
"use strict";
var sharedActions = loop.shared.actions;
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES = {
RECEIVED: "recv",
SENT: "sent"
SENT: "sent",
SPECIAL: "special"
};
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES = {
TEXT: "chat-text"
CONTEXT: "chat-context",
TEXT: "chat-text",
ROOM_NAME: "room-name"
};
/**
@ -27,7 +30,8 @@ loop.store.TextChatStore = (function() {
actions: [
"dataChannelsAvailable",
"receivedTextChatMessage",
"sendTextChatMessage"
"sendTextChatMessage",
"updateRoomInfo"
],
/**
@ -74,21 +78,29 @@ loop.store.TextChatStore = (function() {
/**
* Appends a message to the store, which may be of type 'sent' or 'received'.
*
* @param {String} type
* @param {sharedActions.ReceivedTextChatMessage|sharedActions.SendTextChatMessage} actionData
* @param {CHAT_MESSAGE_TYPES} type
* @param {Object} messageData Data for this message. Options are:
* - {CHAT_CONTENT_TYPES} contentType
* - {String} message The message detail.
* - {Object} extraData Extra data associated with the message.
*/
_appendTextChatMessage: function(type, actionData) {
_appendTextChatMessage: function(type, messageData) {
// We create a new list to avoid updating the store's state directly,
// which confuses the views.
var message = {
type: type,
contentType: actionData.contentType,
message: actionData.message
contentType: messageData.contentType,
message: messageData.message,
extraData: messageData.extraData
};
var newList = this._storeState.messageList.concat(message);
this.setStoreState({ messageList: newList });
window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
// Notify MozLoopService if appropriate that a message has been appended
// and it should therefore check if we need a different sized window or not.
if (type != CHAT_MESSAGE_TYPES.SPECIAL) {
window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
}
},
/**
@ -114,8 +126,38 @@ loop.store.TextChatStore = (function() {
sendTextChatMessage: function(actionData) {
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SENT, actionData);
this._sdkDriver.sendTextChatMessage(actionData);
},
/**
* Handles receiving information about the room - specifically the room name
* so it can be added to the list.
*
* @param {sharedActions.UpdateRoomInfo} actionData
*/
updateRoomInfo: function(actionData) {
// XXX When we add special messages to desktop, we'll need to not post
// multiple changes of room name, only the first. Bug 1171940 should fix this.
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: mozL10n.get("rooms_welcome_title", {conversationName: actionData.roomName})
});
// Append the context if we have any.
if ("urls" in actionData && actionData.urls.length) {
// We only support the first url at the moment.
var urlData = actionData.urls[0];
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: urlData.description,
extraData: {
location: urlData.location,
thumbnail: urlData.thumbnail
}
});
}
}
});
return TextChatStore;
})();
})(navigator.mozL10n || window.mozL10n);

View File

@ -5,8 +5,9 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.TextChatView = (function(mozl10n) {
loop.shared.views.TextChatView = (function(mozL10n) {
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
@ -17,6 +18,7 @@ loop.shared.views.TextChatView = (function(mozl10n) {
mixins: [React.addons.PureRenderMixin],
propTypes: {
contentType: React.PropTypes.string.isRequired,
message: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired
},
@ -24,12 +26,14 @@ loop.shared.views.TextChatView = (function(mozl10n) {
render: function() {
var classes = React.addons.classSet({
"text-chat-entry": true,
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
});
return (
React.createElement("div", {className: classes},
React.createElement("span", null, this.props.message)
React.createElement("p", null, this.props.message)
)
);
}
@ -44,6 +48,7 @@ loop.shared.views.TextChatView = (function(mozl10n) {
mixins: [React.addons.PureRenderMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
messageList: React.PropTypes.array.isRequired
},
@ -76,8 +81,26 @@ loop.shared.views.TextChatView = (function(mozl10n) {
React.createElement("div", {className: "text-chat-scroller"},
this.props.messageList.map(function(entry, i) {
if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL &&
entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) {
return (
React.createElement("div", {className: "context-url-view-wrapper"},
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: entry.message,
dispatcher: this.props.dispatcher,
key: i,
showContextTitle: true,
thumbnail: entry.extraData.thumbnail,
url: entry.extraData.location,
useDesktopPaths: false})
)
);
}
return (
React.createElement(TextChatEntry, {key: i,
contentType: entry.contentType,
message: entry.message,
type: entry.type})
);
@ -90,23 +113,29 @@ loop.shared.views.TextChatView = (function(mozl10n) {
});
/**
* Displays the text chat view. This includes the text chat messages as well
* as a field for entering new messages.
* Displays a text chat entry input box for sending messages.
*
* @property {loop.Dispatcher} dispatcher
* @property {Boolean} showPlaceholder Set to true to show the placeholder message.
* @property {Boolean} textChatEnabled Set to true to enable the box. If false, the
* text chat box won't be displayed.
*/
var TextChatView = React.createClass({displayName: "TextChatView",
var TextChatInputView = React.createClass({displayName: "TextChatInputView",
mixins: [
React.addons.LinkedStateMixin,
loop.store.StoreMixin("textChatStore")
React.addons.PureRenderMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showPlaceholder: React.PropTypes.bool.isRequired,
textChatEnabled: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return _.extend({
return {
messageDetail: ""
}, this.getStoreState());
};
},
/**
@ -139,23 +168,80 @@ loop.shared.views.TextChatView = (function(mozl10n) {
},
render: function() {
if (!this.state.textChatEnabled) {
if (!this.props.textChatEnabled) {
return null;
}
var messageList = this.state.messageList;
return (
React.createElement("div", {className: "text-chat-box"},
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("input", {type: "text",
placeholder: this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : "",
onKeyDown: this.handleKeyDown,
valueLink: this.linkState("messageDetail")})
)
)
);
}
});
/**
* Displays the text chat view. This includes the text chat messages as well
* as a field for entering new messages.
*
* @property {loop.Dispatcher} dispatcher
* @property {Boolean} showAlways If false, the view will not be rendered
* if text chat is not enabled and the
* message list is empty.
* @property {Boolean} showRoomName Set to true to show the room name special
* list item.
*/
var TextChatView = React.createClass({displayName: "TextChatView",
mixins: [
React.addons.LinkedStateMixin,
loop.store.StoreMixin("textChatStore")
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showAlways: React.PropTypes.bool.isRequired,
showRoomName: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
render: function() {
var messageList;
var hasNonSpecialMessages;
if (this.props.showRoomName) {
messageList = this.state.messageList;
hasNonSpecialMessages = messageList.some(function(item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
});
} else {
// XXX Desktop should be showing the initial context here (bug 1171940).
messageList = this.state.messageList.filter(function(item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
});
hasNonSpecialMessages = !!messageList.length;
}
if (!this.props.showAlways && !this.state.textChatEnabled && !messageList.length) {
return null;
}
return (
React.createElement("div", {className: "text-chat-view"},
React.createElement(TextChatEntriesView, {messageList: messageList}),
React.createElement("div", {className: "text-chat-box"},
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("input", {type: "text",
placeholder: messageList.length ? "" : mozl10n.get("chat_textbox_placeholder"),
onKeyDown: this.handleKeyDown,
valueLink: this.linkState("messageDetail")})
)
)
React.createElement(TextChatEntriesView, {
dispatcher: this.props.dispatcher,
messageList: messageList}),
React.createElement(TextChatInputView, {
dispatcher: this.props.dispatcher,
showPlaceholder: !hasNonSpecialMessages,
textChatEnabled: this.state.textChatEnabled})
)
);
}

View File

@ -5,8 +5,9 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.TextChatView = (function(mozl10n) {
loop.shared.views.TextChatView = (function(mozL10n) {
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
@ -17,6 +18,7 @@ loop.shared.views.TextChatView = (function(mozl10n) {
mixins: [React.addons.PureRenderMixin],
propTypes: {
contentType: React.PropTypes.string.isRequired,
message: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired
},
@ -24,12 +26,14 @@ loop.shared.views.TextChatView = (function(mozl10n) {
render: function() {
var classes = React.addons.classSet({
"text-chat-entry": true,
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
});
return (
<div className={classes}>
<span>{this.props.message}</span>
<p>{this.props.message}</p>
</div>
);
}
@ -44,6 +48,7 @@ loop.shared.views.TextChatView = (function(mozl10n) {
mixins: [React.addons.PureRenderMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
messageList: React.PropTypes.array.isRequired
},
@ -76,8 +81,26 @@ loop.shared.views.TextChatView = (function(mozl10n) {
<div className="text-chat-scroller">
{
this.props.messageList.map(function(entry, i) {
if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL &&
entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) {
return (
<div className="context-url-view-wrapper">
<sharedViews.ContextUrlView
allowClick={true}
description={entry.message}
dispatcher={this.props.dispatcher}
key={i}
showContextTitle={true}
thumbnail={entry.extraData.thumbnail}
url={entry.extraData.location}
useDesktopPaths={false} />
</div>
);
}
return (
<TextChatEntry key={i}
contentType={entry.contentType}
message={entry.message}
type={entry.type} />
);
@ -90,23 +113,29 @@ loop.shared.views.TextChatView = (function(mozl10n) {
});
/**
* Displays the text chat view. This includes the text chat messages as well
* as a field for entering new messages.
* Displays a text chat entry input box for sending messages.
*
* @property {loop.Dispatcher} dispatcher
* @property {Boolean} showPlaceholder Set to true to show the placeholder message.
* @property {Boolean} textChatEnabled Set to true to enable the box. If false, the
* text chat box won't be displayed.
*/
var TextChatView = React.createClass({
var TextChatInputView = React.createClass({
mixins: [
React.addons.LinkedStateMixin,
loop.store.StoreMixin("textChatStore")
React.addons.PureRenderMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showPlaceholder: React.PropTypes.bool.isRequired,
textChatEnabled: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return _.extend({
return {
messageDetail: ""
}, this.getStoreState());
};
},
/**
@ -139,23 +168,80 @@ loop.shared.views.TextChatView = (function(mozl10n) {
},
render: function() {
if (!this.state.textChatEnabled) {
if (!this.props.textChatEnabled) {
return null;
}
var messageList = this.state.messageList;
return (
<div className="text-chat-box">
<form onSubmit={this.handleFormSubmit}>
<input type="text"
placeholder={this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : ""}
onKeyDown={this.handleKeyDown}
valueLink={this.linkState("messageDetail")} />
</form>
</div>
);
}
});
/**
* Displays the text chat view. This includes the text chat messages as well
* as a field for entering new messages.
*
* @property {loop.Dispatcher} dispatcher
* @property {Boolean} showAlways If false, the view will not be rendered
* if text chat is not enabled and the
* message list is empty.
* @property {Boolean} showRoomName Set to true to show the room name special
* list item.
*/
var TextChatView = React.createClass({
mixins: [
React.addons.LinkedStateMixin,
loop.store.StoreMixin("textChatStore")
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showAlways: React.PropTypes.bool.isRequired,
showRoomName: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
render: function() {
var messageList;
var hasNonSpecialMessages;
if (this.props.showRoomName) {
messageList = this.state.messageList;
hasNonSpecialMessages = messageList.some(function(item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
});
} else {
// XXX Desktop should be showing the initial context here (bug 1171940).
messageList = this.state.messageList.filter(function(item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
});
hasNonSpecialMessages = !!messageList.length;
}
if (!this.props.showAlways && !this.state.textChatEnabled && !messageList.length) {
return null;
}
return (
<div className="text-chat-view">
<TextChatEntriesView messageList={messageList} />
<div className="text-chat-box">
<form onSubmit={this.handleFormSubmit}>
<input type="text"
placeholder={messageList.length ? "" : mozl10n.get("chat_textbox_placeholder")}
onKeyDown={this.handleKeyDown}
valueLink={this.linkState("messageDetail")} />
</form>
</div>
<TextChatEntriesView
dispatcher={this.props.dispatcher}
messageList={messageList} />
<TextChatInputView
dispatcher={this.props.dispatcher}
showPlaceholder={!hasNonSpecialMessages}
textChatEnabled={this.state.textChatEnabled} />
</div>
);
}

View File

@ -689,6 +689,94 @@ loop.shared.views = (function(_, l10n) {
}
});
/**
* Renders a url that's part of context on the display.
*
* @property {Boolean} allowClick Set to true to allow the url to be clicked. If this
* is specified, then 'dispatcher' is also required.
* @property {String} description The description for the context url.
* @property {loop.Dispatcher} dispatcher
* @property {Boolean} showContextTitle Whether or not to show the "Let's talk about" title.
* @property {String} thumbnail The thumbnail url (expected to be a data url) to
* display. If not specified, a fallback url will be
* shown.
* @property {String} url The url to be displayed. If not present or invalid,
* then this view won't be displayed.
* @property {Boolean} useDesktopPaths Whether or not to use the desktop paths for for the
* fallback url.
*/
var ContextUrlView = React.createClass({displayName: "ContextUrlView",
mixins: [React.addons.PureRenderMixin],
PropTypes: {
allowClick: React.PropTypes.bool.isRequired,
description: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
showContextTitle: React.PropTypes.bool.isRequired,
thumbnail: React.PropTypes.string,
url: React.PropTypes.string,
useDesktopPaths: React.PropTypes.bool.isRequired
},
/**
* Dispatches an action to record when the link is clicked.
*/
handleLinkClick: function() {
if (!this.props.allowClick) {
return;
}
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Shared URL"
}));
},
/**
* Renders the context title ("Let's talk about") if necessary.
*/
renderContextTitle: function() {
if (!this.props.showContextTitle) {
return null;
}
return React.createElement("p", null, l10n.get("context_inroom_label"));
},
render: function() {
var hostname;
try {
hostname = new URL(this.props.url).hostname;
} catch (ex) {
return null;
}
var thumbnail = this.props.thumbnail;
if (!thumbnail) {
thumbnail = this.props.useDesktopPaths ?
"loop/shared/img/icons-16x16.svg#globe" :
"shared/img/icons-16x16.svg#globe";
}
return (
React.createElement("div", {className: "context-content"},
this.renderContextTitle(),
React.createElement("div", {className: "context-wrapper"},
React.createElement("img", {className: "context-preview", src: thumbnail}),
React.createElement("span", {className: "context-description"},
this.props.description,
React.createElement("a", {className: "context-url",
onClick: this.handleLinkClick,
href: this.props.allowClick ? this.props.url : null,
target: "_blank"}, hostname)
)
)
)
);
}
});
/**
* Renders a media element for display. This also handles displaying an avatar
* instead of the video, and attaching a video stream to the video element.
@ -800,6 +888,7 @@ loop.shared.views = (function(_, l10n) {
Button: Button,
ButtonGroup: ButtonGroup,
Checkbox: Checkbox,
ContextUrlView: ContextUrlView,
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,

View File

@ -689,6 +689,94 @@ loop.shared.views = (function(_, l10n) {
}
});
/**
* Renders a url that's part of context on the display.
*
* @property {Boolean} allowClick Set to true to allow the url to be clicked. If this
* is specified, then 'dispatcher' is also required.
* @property {String} description The description for the context url.
* @property {loop.Dispatcher} dispatcher
* @property {Boolean} showContextTitle Whether or not to show the "Let's talk about" title.
* @property {String} thumbnail The thumbnail url (expected to be a data url) to
* display. If not specified, a fallback url will be
* shown.
* @property {String} url The url to be displayed. If not present or invalid,
* then this view won't be displayed.
* @property {Boolean} useDesktopPaths Whether or not to use the desktop paths for for the
* fallback url.
*/
var ContextUrlView = React.createClass({
mixins: [React.addons.PureRenderMixin],
PropTypes: {
allowClick: React.PropTypes.bool.isRequired,
description: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
showContextTitle: React.PropTypes.bool.isRequired,
thumbnail: React.PropTypes.string,
url: React.PropTypes.string,
useDesktopPaths: React.PropTypes.bool.isRequired
},
/**
* Dispatches an action to record when the link is clicked.
*/
handleLinkClick: function() {
if (!this.props.allowClick) {
return;
}
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Shared URL"
}));
},
/**
* Renders the context title ("Let's talk about") if necessary.
*/
renderContextTitle: function() {
if (!this.props.showContextTitle) {
return null;
}
return <p>{l10n.get("context_inroom_label")}</p>;
},
render: function() {
var hostname;
try {
hostname = new URL(this.props.url).hostname;
} catch (ex) {
return null;
}
var thumbnail = this.props.thumbnail;
if (!thumbnail) {
thumbnail = this.props.useDesktopPaths ?
"loop/shared/img/icons-16x16.svg#globe" :
"shared/img/icons-16x16.svg#globe";
}
return (
<div className="context-content">
{this.renderContextTitle()}
<div className="context-wrapper">
<img className="context-preview" src={thumbnail} />
<span className="context-description">
{this.props.description}
<a className="context-url"
onClick={this.handleLinkClick}
href={this.props.allowClick ? this.props.url : null}
target="_blank">{hostname}</a>
</span>
</div>
</div>
);
}
});
/**
* Renders a media element for display. This also handles displaying an avatar
* instead of the video, and attaching a video stream to the video element.
@ -800,6 +888,7 @@ loop.shared.views = (function(_, l10n) {
Button: Button,
ButtonGroup: ButtonGroup,
Checkbox: Checkbox,
ContextUrlView: ContextUrlView,
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,

View File

@ -365,13 +365,12 @@ p.standalone-btn-label {
*/
.text-chat-view {
height: 60px;
color: white;
color: black;
}
.text-chat-entries {
/* XXX Should use flex, this is just for the initial implementation. */
height: calc(100% - 2em);
width: 30%;
}
.text-chat-box {

View File

@ -666,10 +666,15 @@ loop.standaloneRoomViews = (function(mozL10n) {
hide: !this.state.receivingScreenShare
});
// XXX Temporarily showAlways = showRoomName = false for TextChatView
// until bug 1168829 is completed.
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement("div", {className: "beta-logo"}),
React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher}),
React.createElement(sharedViews.TextChatView, {
dispatcher: this.props.dispatcher,
showAlways: false,
showRoomName: false}),
React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}),
React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState,
failureReason: this.state.failureReason,

View File

@ -666,10 +666,15 @@ loop.standaloneRoomViews = (function(mozL10n) {
hide: !this.state.receivingScreenShare
});
// XXX Temporarily showAlways = showRoomName = false for TextChatView
// until bug 1168829 is completed.
return (
<div className="room-conversation-wrapper">
<div className="beta-logo" />
<sharedViews.TextChatView dispatcher={this.props.dispatcher} />
<sharedViews.TextChatView
dispatcher={this.props.dispatcher}
showAlways={false}
showRoomName={false} />
<StandaloneRoomHeader dispatcher={this.props.dispatcher} />
<StandaloneRoomInfoArea roomState={this.state.roomState}
failureReason={this.state.failureReason}

View File

@ -111,6 +111,9 @@ help_label=Help
tour_label=Tour
rooms_default_room_name_template=Conversation {{conversationLabel}}
## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
## by the user specified conversation name.
rooms_welcome_title=Welcome to {{conversationName}}
rooms_leave_button_label=Leave
rooms_list_copy_url_tooltip=Copy Link
rooms_list_delete_tooltip=Delete conversation
@ -141,3 +144,9 @@ support_link=Get Help
# Text chat strings
chat_textbox_placeholder=Type here…
# LOCALIZATION NOTE (context_inroom_label): this string is followed by the
# title/URL of the website you are having a conversation about, displayed on a
# separate line. If this structure doesn't work for your locale, you might want
# to consider this as a stand-alone title. See example screenshot:
# https://bug1084991.bugzilla.mozilla.org/attachment.cgi?id=8614721
context_inroom_label=Let's talk about:

View File

@ -64,7 +64,8 @@ describe("loop.store.TextChatStore", function () {
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.TEXT,
message: message
message: message,
extraData: undefined
}]);
});
@ -113,7 +114,8 @@ describe("loop.store.TextChatStore", function () {
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SENT,
contentType: messageData.contentType,
message: messageData.message
message: messageData.message,
extraData: undefined
}]);
});
@ -128,4 +130,67 @@ describe("loop.store.TextChatStore", function () {
new CustomEvent("LoopChatMessageAppended"));
});
});
describe("#updateRoomInfo", function() {
it("should add the room name to the list", function() {
sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomOwner: "Mark",
roomUrl: "fake"
}));
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: "Let's really share!",
extraData: undefined
}]);
});
it("should add the context to the list", function() {
sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomOwner: "Mark",
roomUrl: "fake",
urls: [{
description: "A wonderful event",
location: "http://wonderful.invalid",
thumbnail: "fake"
}]
}));
expect(store.getStoreState("messageList")).eql([
{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: "Let's really share!",
extraData: undefined
}, {
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event",
extraData: {
location: "http://wonderful.invalid",
thumbnail: "fake"
}
}
]);
});
it("should not dispatch a LoopChatMessageAppended event", function() {
sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomOwner: "Mark",
roomUrl: "fake"
}));
sinon.assert.notCalled(window.dispatchEvent);
});
});
});

View File

@ -6,6 +6,7 @@ describe("loop.shared.views.TextChatView", function () {
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
var TestUtils = React.addons.TestUtils;
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
@ -39,25 +40,118 @@ describe("loop.shared.views.TextChatView", function () {
describe("TextChatView", function() {
var view;
function mountTestComponent() {
function mountTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.shared.views.TextChatView, {
dispatcher: dispatcher
}));
React.createElement(loop.shared.views.TextChatView, props));
}
beforeEach(function() {
store.setStoreState({ textChatEnabled: true });
});
it("should not display anything if text chat is disabled", function() {
it("should not display anything if no messages and text chat not enabled and showAlways is false", function() {
store.setStoreState({ textChatEnabled: false });
view = mountTestComponent();
view = mountTestComponent({
showAlways: false
});
expect(view.getDOMNode()).eql(null);
});
it("should display the view if no messages and text chat not enabled and showAlways is true", function() {
store.setStoreState({ textChatEnabled: false });
view = mountTestComponent({
showAlways: true
});
expect(view.getDOMNode()).not.eql(null);
});
it("should display the view if text chat is enabled", function() {
view = mountTestComponent({
showAlways: true
});
expect(view.getDOMNode()).not.eql(null);
});
it("should display only the text chat box if entry is enabled but there are no messages", function() {
view = mountTestComponent();
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-box")).not.eql(null);
expect(node.querySelector(".text-chat-entries")).eql(null);
});
it("should render message entries when message were sent/ received", function() {
view = mountTestComponent();
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Is it me you're looking for?"
});
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
var entries = node.querySelectorAll(".text-chat-entry");
expect(entries.length).to.eql(2);
expect(entries[0].classList.contains("received")).to.eql(true);
expect(entries[1].classList.contains("received")).to.not.eql(true);
});
it("should render a room name special entry", function() {
view = mountTestComponent({
showRoomName: true
});
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A wonderful surprise!",
roomOwner: "Chris",
roomUrl: "Fake"
}));
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
var entries = node.querySelectorAll(".text-chat-entry");
expect(entries.length).eql(1);
expect(entries[0].classList.contains("special")).eql(true);
expect(entries[0].classList.contains("room-name")).eql(true);
});
it("should render a special entry for the context url", function() {
view = mountTestComponent({
showRoomName: true
});
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
roomOwner: "fake",
roomUrl: "http://showcase",
urls: [{
description: "A wonderful page!",
location: "http://wonderful.invalid"
// use the fallback thumbnail
}]
}));
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
expect(node.querySelector(".context-url-view-wrapper")).to.not.eql(null);
});
it("should dispatch SendTextChatMessage action when enter is pressed", function() {
view = mountTestComponent();
@ -80,32 +174,5 @@ describe("loop.shared.views.TextChatView", function () {
message: "Hello!"
}));
});
it("should not render message entries when none are sent/ received yet", function() {
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".text-chat-entries")).to.eql(null);
});
it("should render message entries when message were sent/ received", function() {
view = mountTestComponent();
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Is it me you're looking for?"
});
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
var entries = node.querySelectorAll(".text-chat-entry");
expect(entries.length).to.eql(2);
expect(entries[0].classList.contains("received")).to.eql(true);
expect(entries[1].classList.contains("received")).to.not.eql(true);
});
});
});

View File

@ -815,6 +815,89 @@ describe("loop.shared.views", function() {
});
});
describe("ContextUrlView", function() {
var view;
function mountTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(sharedViews.ContextUrlView, props));
}
it("should display nothing if the url is invalid", function() {
view = mountTestComponent({
url: "fjrTykyw"
});
expect(view.getDOMNode()).eql(null);
});
it("should use a default thumbnail if one is not supplied", function() {
view = mountTestComponent({
url: "http://wonderful.invalid"
});
expect(view.getDOMNode().querySelector(".context-preview").getAttribute("src"))
.eql("shared/img/icons-16x16.svg#globe");
});
it("should use a default thumbnail for desktop if one is not supplied", function() {
view = mountTestComponent({
useDesktopPaths: true,
url: "http://wonderful.invalid"
});
expect(view.getDOMNode().querySelector(".context-preview").getAttribute("src"))
.eql("loop/shared/img/icons-16x16.svg#globe");
});
it("should not display a title if by default", function() {
view = mountTestComponent({
url: "http://wonderful.invalid"
});
expect(view.getDOMNode().querySelector(".context-content > p")).eql(null);
});
it("should display a title if required", function() {
view = mountTestComponent({
showContextTitle: true,
url: "http://wonderful.invalid"
});
expect(view.getDOMNode().querySelector(".context-content > p")).not.eql(null);
});
it("should set the href on the link if clicks are allowed", function() {
view = mountTestComponent({
allowClick: true,
url: "http://wonderful.invalid"
});
expect(view.getDOMNode().querySelector(".context-url").href)
.eql("http://wonderful.invalid/");
});
it("should dispatch an action to record link clicks", function() {
view = mountTestComponent({
allowClick: true,
url: "http://wonderful.invalid"
});
var linkNode = view.getDOMNode().querySelector(".context-url");
TestUtils.Simulate.click(linkNode);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Shared URL"
}));
});
});
describe("MediaView", function() {
var view;

View File

@ -151,3 +151,8 @@ body {
margin-left: .5rem;
border: 0;
}
/* Temporary until bug 1168829 is completed */
.standalone.text-chat-example .text-chat-view {
height: 400px;
}

View File

@ -11,6 +11,8 @@
document.removeEventListener("DOMContentLoaded", loop.panel.init);
document.removeEventListener("DOMContentLoaded", loop.conversation.init);
var sharedActions = loop.shared.actions;
// 1. Desktop components
// 1.1 Panel
var PanelView = loop.panel.PanelView;
@ -32,6 +34,7 @@
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var FeedbackView = loop.shared.views.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
var TextChatView = loop.shared.views.TextChatView;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
@ -254,6 +257,18 @@
textChatEnabled: false
});
// Update the text chat store with the room info.
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
roomOwner: "fake",
roomUrl: "http://showcase",
urls: [{
description: "A wonderful page!",
location: "http://wonderful.invalid"
// use the fallback thumbnail
}]
}));
loop.store.StoreMixin.register({
conversationStore: conversationStore,
feedbackStore: feedbackStore,
@ -937,6 +952,18 @@
)
),
React.createElement(Section, {name: "TextChatView (standalone)"},
React.createElement(FramedExample, {width: 200, height: 400,
summary: "Standalone Text Chat conversation (200 x 400)"},
React.createElement("div", {className: "standalone text-chat-example"},
React.createElement(TextChatView, {
dispatcher: dispatcher,
showAlways: true,
showRoomName: true})
)
)
),
React.createElement(Section, {name: "SVG icons preview", className: "svg-icons"},
React.createElement(Example, {summary: "10x10"},
React.createElement(SVGIcons, {size: "10x10"})

View File

@ -11,6 +11,8 @@
document.removeEventListener("DOMContentLoaded", loop.panel.init);
document.removeEventListener("DOMContentLoaded", loop.conversation.init);
var sharedActions = loop.shared.actions;
// 1. Desktop components
// 1.1 Panel
var PanelView = loop.panel.PanelView;
@ -32,6 +34,7 @@
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var FeedbackView = loop.shared.views.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
var TextChatView = loop.shared.views.TextChatView;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
@ -254,6 +257,18 @@
textChatEnabled: false
});
// Update the text chat store with the room info.
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
roomOwner: "fake",
roomUrl: "http://showcase",
urls: [{
description: "A wonderful page!",
location: "http://wonderful.invalid"
// use the fallback thumbnail
}]
}));
loop.store.StoreMixin.register({
conversationStore: conversationStore,
feedbackStore: feedbackStore,
@ -937,6 +952,18 @@
</FramedExample>
</Section>
<Section name="TextChatView (standalone)">
<FramedExample width={200} height={400}
summary="Standalone Text Chat conversation (200 x 400)">
<div className="standalone text-chat-example">
<TextChatView
dispatcher={dispatcher}
showAlways={true}
showRoomName={true} />
</div>
</FramedExample>
</Section>
<Section name="SVG icons preview" className="svg-icons">
<Example summary="10x10">
<SVGIcons size="10x10"/>

View File

@ -61,7 +61,9 @@ let TabAttributesInternal = {
// Set attributes.
for (let name in data) {
tab.setAttribute(name, data[name]);
if (!this._skipAttrs.has(name)) {
tab.setAttribute(name, data[name]);
}
}
}
};

View File

@ -233,7 +233,7 @@ let TabStateInternal = {
if (value.hasOwnProperty("index")) {
tabData.index = value.index;
}
} else if (value) {
} else {
tabData[key] = value;
}
}

View File

@ -36,7 +36,8 @@ add_task(function* test() {
// Make sure we're backwards compatible and restore old 'image' attributes.
let state = {
entries: [{url: "about:mozilla"}],
attributes: {custom: "foobaz", image: gBrowser.getIcon(tab)}
attributes: {custom: "foobaz"},
image: gBrowser.getIcon(tab)
};
// Prepare a pending tab waiting to be restored.
@ -45,7 +46,8 @@ add_task(function* test() {
yield promise;
ok(tab.hasAttribute("pending"), "tab is pending");
is(gBrowser.getIcon(tab), state.attributes.image, "tab has correct icon");
is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
ok(!state.attributes.image, "'image' attribute not saved");
// Let the pending tab load.
gBrowser.selectedTab = tab;

View File

@ -27,6 +27,9 @@ add_task(function* () {
let tabState = TabState.collect(tab);
is(tabState.index, TAB_STATE.index, "correct shistory index");
// Check we don't collect userTypedValue when we shouldn't.
ok(!tabState.userTypedValue, "tab didn't have a userTypedValue");
// Cleanup.
gBrowser.removeTab(tab);
});

View File

@ -45,6 +45,7 @@ support-files =
code_ugly-8^headers^
code_WorkerActor.attach-worker1.js
code_WorkerActor.attach-worker2.js
code_WorkerActor.attachThread-worker.js
doc_auto-pretty-print-01.html
doc_auto-pretty-print-02.html
doc_binary_search.html
@ -107,6 +108,7 @@ support-files =
doc_with-frame.html
doc_WorkerActor.attach-tab1.html
doc_WorkerActor.attach-tab2.html
doc_WorkerActor.attachThread-tab.html
head.js
sjs_random-javascript.sjs
testactors.js
@ -566,3 +568,5 @@ skip-if = e10s && debug
skip-if = e10s && debug
[browser_dbg_WorkerActor.attach.js]
skip-if = e10s && debug
[browser_dbg_WorkerActor.attachThread.js]
skip-if = e10s && debug

View File

@ -27,7 +27,6 @@ function test() {
// registered. Instead, we have to wait for the promise returned by
// createWorker in the tab to be resolved.
yield createWorkerInTab(tab, WORKER1_URL);
let { workers } = yield listWorkers(tabClient);
let [, workerClient1] = yield attachWorker(tabClient,
findWorker(workers, WORKER1_URL));

View File

@ -0,0 +1,89 @@
let TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
let WORKER_URL = "code_WorkerActor.attachThread-worker.js";
function test() {
Task.spawn(function* () {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
let client1 = new DebuggerClient(DebuggerServer.connectPipe());
yield connect(client1);
let client2 = new DebuggerClient(DebuggerServer.connectPipe());
yield connect(client2);
let tab = yield addTab(TAB_URL);
let { tabs: tabs1 } = yield listTabs(client1);
let [, tabClient1] = yield attachTab(client1, findTab(tabs1, TAB_URL));
let { tabs: tabs2 } = yield listTabs(client2);
let [, tabClient2] = yield attachTab(client2, findTab(tabs2, TAB_URL));
yield listWorkers(tabClient1);
yield listWorkers(tabClient2);
yield createWorkerInTab(tab, WORKER_URL);
let { workers: workers1 } = yield listWorkers(tabClient1);
let [, workerClient1] = yield attachWorker(tabClient1,
findWorker(workers1, WORKER_URL));
let { workers: workers2 } = yield listWorkers(tabClient2);
let [, workerClient2] = yield attachWorker(tabClient2,
findWorker(workers2, WORKER_URL));
let location = { line: 5 };
let [, threadClient1] = yield attachThread(workerClient1);
let sources1 = yield getSources(threadClient1);
let sourceClient1 = threadClient1.source(findSource(sources1,
EXAMPLE_URL + WORKER_URL));
let [, breakpointClient1] = yield setBreakpoint(sourceClient1, location);
yield resume(threadClient1);
let [, threadClient2] = yield attachThread(workerClient2);
let sources2 = yield getSources(threadClient2);
let sourceClient2 = threadClient2.source(findSource(sources2,
EXAMPLE_URL + WORKER_URL));
let [, breakpointClient2] = yield setBreakpoint(sourceClient2, location);
yield resume(threadClient2);
postMessageToWorkerInTab(tab, WORKER_URL, "ping");
yield Promise.all([
waitForPause(threadClient1).then((packet) => {
is(packet.type, "paused");
let why = packet.why;
is(why.type, "breakpoint");
is(why.actors.length, 1);
is(why.actors[0], breakpointClient1.actor);
let frame = packet.frame;
let where = frame.where;
is(where.source.actor, sourceClient1.actor);
is(where.line, location.line);
let variables = frame.environment.bindings.variables;
is(variables.a.value, 1);
is(variables.b.value.type, "undefined");
is(variables.c.value.type, "undefined");
return resume(threadClient1);
}),
waitForPause(threadClient2).then((packet) => {
is(packet.type, "paused");
let why = packet.why;
is(why.type, "breakpoint");
is(why.actors.length, 1);
is(why.actors[0], breakpointClient2.actor);
let frame = packet.frame;
let where = frame.where;
is(where.source.actor, sourceClient2.actor);
is(where.line, location.line);
let variables = frame.environment.bindings.variables;
is(variables.a.value, 1);
is(variables.b.value.type, "undefined");
is(variables.c.value.type, "undefined");
return resume(threadClient2);
}),
]);
terminateWorkerInTab(tab, WORKER_URL);
yield waitForWorkerClose(workerClient1);
yield waitForWorkerClose(workerClient2);
yield close(client1);
yield close(client2);
finish();
});
}

View File

@ -0,0 +1,16 @@
"use strict";
function f() {
var a = 1;
var b = 2;
var c = 3;
}
self.onmessage = function (event) {
if (event.data == "ping") {
f()
postMessage("pong");
}
};
postMessage("load");

View File

@ -83,3 +83,15 @@ addMessageListener("jsonrpc", function ({ data: { method, params, id } }) {
});
});
});
addMessageListener("test:postMessageToWorker", function (message) {
dump("Posting message '" + message.data.message + "' to worker with url '" +
message.data.url + "'.\n");
let worker = workers[message.data.url];
worker.postMessage(message.data.message);
worker.addEventListener("message", function listener() {
worker.removeEventListener("message", listener);
sendAsyncMessage("test:postMessageToWorker");
});
});

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
</head>
<body>
</body>
</html>

View File

@ -512,9 +512,13 @@ function getTab(aTarget, aWindow) {
}
function getSources(aClient) {
info("Getting sources.");
let deferred = promise.defer();
aClient.getSources(({sources}) => deferred.resolve(sources));
aClient.getSources((packet) => {
deferred.resolve(packet.sources);
});
return deferred.promise;
}
@ -1129,6 +1133,15 @@ function waitForWorkerListChanged(tabClient) {
});
}
function attachThread(workerClient, options) {
info("Attaching to thread.");
return new Promise(function(resolve, reject) {
workerClient.attachThread(options, function (response, threadClient) {
resolve([response, threadClient]);
});
});
}
function waitForWorkerClose(workerClient) {
info("Waiting for worker to close.");
return new Promise(function (resolve) {
@ -1156,3 +1169,52 @@ function waitForWorkerThaw(workerClient) {
});
});
}
function resume(threadClient) {
info("Resuming thread.");
return rdpInvoke(threadClient, threadClient.resume);
}
function findSource(sources, url) {
info("Finding source with url '" + url + "'.\n");
for (let source of sources) {
if (source.url === url) {
return source;
}
}
return null;
}
function setBreakpoint(sourceClient, location) {
info("Setting breakpoint.\n");
return new Promise(function (resolve) {
sourceClient.setBreakpoint(location, function (response, breakpointClient) {
resolve([response, breakpointClient]);
});
});
}
function waitForEvent(client, type, predicate) {
return new Promise(function (resolve) {
function listener(type, packet) {
if (!predicate(packet)) {
return;
}
client.removeListener(listener);
resolve(packet);
}
if (predicate) {
client.addListener(type, listener);
} else {
client.addOneTimeListener(type, function (type, packet) {
resolve(packet);
});
}
});
}
function waitForPause(threadClient) {
info("Waiting for pause.\n");
return waitForEvent(threadClient, "paused");
}

View File

@ -31,7 +31,6 @@ browser.jar:
content/browser/devtools/ruleview.css (styleinspector/ruleview.css)
content/browser/devtools/layoutview/view.js (layoutview/view.js)
content/browser/devtools/layoutview/view.xhtml (layoutview/view.xhtml)
content/browser/devtools/layoutview/view.css (layoutview/view.css)
content/browser/devtools/fontinspector/font-inspector.js (fontinspector/font-inspector.js)
content/browser/devtools/fontinspector/font-inspector.xhtml (fontinspector/font-inspector.xhtml)
content/browser/devtools/fontinspector/font-inspector.css (fontinspector/font-inspector.css)

View File

@ -1,266 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
body {
max-width: 320px;
position: relative;
margin: 0px auto;
padding: 0;
}
#header {
box-sizing: border-box;
width: 100%;
padding: 4px 13px;
display: -moz-box;
vertical-align: top;
}
#header:-moz-dir(rtl) {
-moz-box-direction: reverse;
}
#header > span {
display: -moz-box;
}
#element-size {
-moz-box-flex: 1;
}
#element-size:-moz-dir(rtl) {
-moz-box-pack: end;
}
#main {
margin: 0 14px 10px 14px;
box-sizing: border-box;
width: calc(100% - 2 * 14px);
position: absolute;
border-width: 1px;
}
#content,
#borders {
border-width: 1px;
}
#content {
height: 25px;
}
#margins,
#padding {
border-style: solid;
border-width: 25px;
}
#borders {
padding: 25px;
}
.legend {
position: absolute;
margin: 5px 6px;
z-index: 1;
}
.legend[data-box="margin"] {
color: var(--theme-highlight-blue);
}
#main > p {
position: absolute;
pointer-events: none;
}
#main > p {
margin: 0;
text-align: center;
}
#main > p > span {
vertical-align: middle;
pointer-events: auto;
}
.size > span {
cursor: default;
}
.editable {
-moz-user-select: text;
}
.top,
.bottom {
width: calc(100% - 2px);
text-align: center;
}
.padding.top {
top: 55px;
}
.padding.bottom {
bottom: 57px;
}
.border.top {
top: 30px;
}
.border.bottom {
bottom: 31px;
}
.margin.top {
top: 5px;
}
.margin.bottom {
bottom: 6px;
}
.size,
.margin.left,
.margin.right,
.border.left,
.border.right,
.padding.left,
.padding.right {
top: 22px;
line-height: 132px;
}
.size {
width: calc(100% - 2px);
}
.margin.right,
.margin.left,
.border.left,
.border.right,
.padding.right,
.padding.left {
width: 25px;
}
.padding.left {
left: 52px;
}
.padding.right {
right: 51px;
}
.border.left {
left: 26px;
}
.border.right {
right: 26px;
}
.margin.right {
right: 0;
}
.margin.left {
left: 0;
}
.rotate.left:not(.editing) {
transform: rotate(-90deg);
}
.rotate.right:not(.editing) {
transform: rotate(90deg);
}
body.dim > #header > #element-position,
body.dim > #main > p {
visibility: hidden;
}
@media (max-height: 228px) {
#header {
padding-top: 0;
padding-bottom: 0;
margin-top: 10px;
margin-bottom: 8px;
}
#margins,
#padding {
border-width: 21px;
}
#borders {
padding: 21px;
}
#content {
height: 21px;
}
.padding.top {
top: 46px;
}
.padding.bottom {
bottom: 46px;
}
.border.top {
top: 25px;
}
.border.bottom {
bottom: 25px;
}
.margin.top {
top: 4px;
}
.margin.bottom {
bottom: 4px;
}
.size,
.margin.left,
.margin.right,
.border.left,
.border.right,
.padding.left,
.padding.right {
line-height: 106px;
}
.margin.right,
.margin.left,
.border.left,
.border.right,
.padding.right,
.padding.left {
width: 21px;
}
.padding.left {
left: 43px;
}
.padding.right {
right: 43px;
}
.border.left {
left: 22px;
}
.border.right {
right: 22px;
}
}

View File

@ -3,25 +3,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals ViewHelpers, window, document */
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
const Cc = Components.classes;
const {utils: Cu, interfaces: Ci, classes: Cc} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
const {ReflowFront} = devtools.require("devtools/server/actors/layout");
const {require} = devtools;
const {InplaceEditor, editableItem} = require("devtools/shared/inplace-editor");
const {ReflowFront} = require("devtools/server/actors/layout");
const SHARED_L10N = new ViewHelpers.L10N("chrome://browser/locale/devtools/shared.properties");
const STRINGS_URI = "chrome://browser/locale/devtools/shared.properties";
const SHARED_L10N = new ViewHelpers.L10N(STRINGS_URI);
const NUMERIC = /^-?[\d\.]+$/;
const LONG_TEXT_ROTATE_LIMIT = 3;
@ -205,13 +203,14 @@ LayoutView.prototype = {
value: undefined},
borderRight: {selector: ".border.right > span",
property: "border-right-width",
value: undefined},
value: undefined}
};
// Make each element the dimensions editable
for (let i in this.map) {
if (i == "position")
if (i == "position") {
continue;
}
let dimension = this.map[i];
editableItem({
@ -231,7 +230,8 @@ LayoutView.prototype = {
if (!this.reflowFront) {
let toolbox = this.inspector.toolbox;
if (toolbox.target.form.reflowActor) {
this.reflowFront = ReflowFront(toolbox.target.client, toolbox.target.form);
this.reflowFront = ReflowFront(toolbox.target.client,
toolbox.target.form);
} else {
return;
}
@ -265,11 +265,11 @@ LayoutView.prototype = {
element: element,
initial: initialValue,
start: (editor) => {
start: editor => {
editor.elt.parentNode.classList.add("editing");
},
change: (value) => {
change: value => {
if (NUMERIC.test(value)) {
value += "px";
}
@ -300,13 +300,25 @@ LayoutView.prototype = {
},
/**
* Is the layoutview visible in the sidebar?
* Is the layoutview visible in the sidebar.
* @return {Boolean}
*/
isActive: function() {
isViewVisible: function() {
return this.inspector &&
this.inspector.sidebar.getCurrentTabID() == "layoutview";
},
/**
* Is the layoutview visible in the sidebar and is the current node valid to
* be displayed in the view.
* @return {Boolean}
*/
isViewVisibleAndNodeValid: function() {
return this.isViewVisible() &&
this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode();
},
/**
* Destroy the nodes. Remove listeners.
*/
@ -328,9 +340,7 @@ LayoutView.prototype = {
},
onSidebarSelect: function(e, sidebar) {
if (sidebar !== "layoutview") {
this.dim();
}
this.setActive(sidebar === "layoutview");
},
/**
@ -338,42 +348,37 @@ LayoutView.prototype = {
*/
onNewSelection: function() {
let done = this.inspector.updating("layoutview");
this.onNewNode().then(done, (err) => { console.error(err); done() });
this.onNewNode().then(done, err => {
console.error(err);
done();
});
},
/**
* @return a promise that resolves when the view has been updated
*/
onNewNode: function() {
if (this.isActive() &&
this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode()) {
this.undim();
} else {
this.dim();
}
this.setActive(this.isViewVisibleAndNodeValid());
return this.update();
},
/**
* Hide the layout boxes and stop refreshing on reflows. No node is selected
* or the layout-view sidebar is inactive.
* Stop tracking reflows and hide all values when no node is selected or the
* layout-view is hidden, otherwise track reflows and show values.
* @param {Boolean} isActive
*/
dim: function() {
this.untrackReflows();
this.doc.body.classList.add("dim");
this.dimmed = true;
},
setActive: function(isActive) {
if (isActive === this.isActive) {
return;
}
this.isActive = isActive;
/**
* Show the layout boxes and start refreshing on reflows. A node is selected
* and the layout-view side is active.
*/
undim: function() {
this.trackReflows();
this.doc.body.classList.remove("dim");
this.dimmed = false;
this.doc.body.classList.toggle("inactive", !isActive);
if (isActive) {
this.trackReflows();
} else {
this.untrackReflows();
}
},
/**
@ -383,15 +388,13 @@ LayoutView.prototype = {
*/
update: function() {
let lastRequest = Task.spawn((function*() {
if (!this.isActive() ||
!this.inspector.selection.isConnected() ||
!this.inspector.selection.isElementNode()) {
if (!this.isViewVisibleAndNodeValid()) {
return;
}
let node = this.inspector.selection.nodeFront;
let layout = yield this.inspector.pageStyle.getLayout(node, {
autoMargins: !this.dimmed
autoMargins: this.isActive
});
let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
@ -409,8 +412,8 @@ LayoutView.prototype = {
this.sizeHeadingLabel.textContent = newLabel;
}
// If the view is dimmed, no need to do anything more.
if (this.dimmed) {
// If the view isn't active, no need to do anything more.
if (!this.isActive) {
this.inspector.emit("layoutview-updated");
return null;
}
@ -433,10 +436,18 @@ LayoutView.prototype = {
}
let margins = layout.autoMargins;
if ("top" in margins) this.map.marginTop.value = "auto";
if ("right" in margins) this.map.marginRight.value = "auto";
if ("bottom" in margins) this.map.marginBottom.value = "auto";
if ("left" in margins) this.map.marginLeft.value = "auto";
if ("top" in margins) {
this.map.marginTop.value = "auto";
}
if ("right" in margins) {
this.map.marginRight.value = "auto";
}
if ("bottom" in margins) {
this.map.marginBottom.value = "auto";
}
if ("left" in margins) {
this.map.marginLeft.value = "auto";
}
for (let i in this.map) {
let selector = this.map[i].selector;
@ -538,14 +549,17 @@ let elts;
let onmouseover = function(e) {
let region = e.target.getAttribute("data-box");
if (!region) {
return false;
}
this.layoutview.showBoxModel({region});
return false;
}.bind(window);
let onmouseout = function(e) {
let onmouseout = function() {
this.layoutview.hideBoxModel();
return false;
}.bind(window);
@ -561,8 +575,8 @@ window.setPanel = function(panel) {
}
// Mark document as RTL or LTR:
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry);
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
let dir = chromeReg.isLocaleRTL("global");
document.body.setAttribute("dir", dir ? "rtl" : "ltr");

View File

@ -19,7 +19,6 @@
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/layoutview.css" type="text/css"/>
<link rel="stylesheet" href="view.css" type="text/css"/>
</head>
<body class="theme-sidebar devtools-monospace">

View File

@ -210,6 +210,19 @@ const JITOptimizations = function (rawSites, stringTable) {
this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);;
};
/**
* Make JITOptimizations iterable.
*/
JITOptimizations.prototype = {
[Symbol.iterator]: function *() {
yield* this.optimizationSites;
},
get length() {
return this.optimizationSites.length;
}
};
/**
* Takes an "outcome" string from an OptimizationAttempt and returns
* a boolean indicating whether or not its a successful outcome.

View File

@ -235,9 +235,9 @@ ThreadNode.prototype = {
leafTable);
if (isLeaf) {
frameNode.youngestFrameSamples++;
frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
}
frameNode.samples++;
frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
prevFrameKey = frameKey;
prevCalls = frameNode.calls;

View File

@ -31,12 +31,14 @@ function* spawnTest() {
yield injectAndRenderProfilerData();
// gRawSite1 and gRawSite2 are both optimizations on A, so they'll have
// indices in descending order of # of samples.
yield checkFrame(1, [{ i: 0, opt: gRawSite1 }, { i: 1, opt: gRawSite2 }]);
// A is never a leaf, so it's optimizations should not be shown.
yield checkFrame(1);
// gRawSite3 is the only optimization on B, so it'll have index 0.
yield checkFrame(2, [{ i: 0, opt: gRawSite3 }]);
// gRawSite2 and gRawSite3 are both optimizations on B, so they'll have
// indices in descending order of # of samples.
yield checkFrame(2, [{ i: 0, opt: gRawSite2 }, { i: 1, opt: gRawSite3 }]);
// Leaf node (C) with no optimizations should not display any opts.
yield checkFrame(3);
let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
@ -114,7 +116,7 @@ function* spawnTest() {
// The second and third optimization should display optimization failures.
let warningIcon = $(`.tree-widget-container li[data-id='["${i}"]'] .opt-icon[severity=warning]`);
if (opt === gRawSite2 || opt === gRawSite3) {
if (opt === gRawSite3 || opt === gRawSite1) {
ok(warningIcon, "did find a warning icon for all strategies failing.");
} else {
ok(!warningIcon, "did not find a warning icon for no successful strategies");
@ -130,7 +132,7 @@ function uniqStr(s) {
}
// Since deflateThread doesn't handle deflating optimization info, use
// placeholder names A_O1, B_O3, and A_O2, which will be used to manually
// placeholder names A_O1, B_O2, and B_O3, which will be used to manually
// splice deduped opts into the profile.
let gThread = RecordingUtils.deflateThread({
samples: [{
@ -143,7 +145,7 @@ let gThread = RecordingUtils.deflateThread({
frames: [
{ location: "(root)" },
{ location: "A_O1" },
{ location: "B_O3" },
{ location: "B_O2" },
{ location: "C (http://foo/bar/baz:56)" }
]
}, {
@ -151,14 +153,14 @@ let gThread = RecordingUtils.deflateThread({
frames: [
{ location: "(root)" },
{ location: "A (http://foo/bar/baz:12)" },
{ location: "B (http://foo/bar/boo:34)" },
{ location: "B_O2" },
]
}, {
time: 5 + 1 + 2,
frames: [
{ location: "(root)" },
{ location: "A_O2" },
{ location: "B (http://foo/bar/boo:34)" },
{ location: "A_O1" },
{ location: "B_O3" },
]
}, {
time: 5 + 1 + 2 + 7,
@ -197,14 +199,14 @@ let gRawSite1 = {
data: [
[uniqStr("Failure1"), uniqStr("SomeGetter1")],
[uniqStr("Failure2"), uniqStr("SomeGetter2")],
[uniqStr("Inlined"), uniqStr("SomeGetter3")]
[uniqStr("Failure3"), uniqStr("SomeGetter3")]
]
}
};
let gRawSite2 = {
_testFrameInfo: { name: "A", line: "12", file: "@baz" },
line: 12,
_testFrameInfo: { name: "B", line: "10", file: "@boo" },
line: 40,
types: [{
mirType: uniqStr("Int32"),
site: uniqStr("Receiver")
@ -217,13 +219,13 @@ let gRawSite2 = {
data: [
[uniqStr("Failure1"), uniqStr("SomeGetter1")],
[uniqStr("Failure2"), uniqStr("SomeGetter2")],
[uniqStr("Failure3"), uniqStr("SomeGetter3")]
[uniqStr("Inlined"), uniqStr("SomeGetter3")]
]
}
};
let gRawSite3 = {
_testFrameInfo: { name: "B", line: "34", file: "@boo" },
_testFrameInfo: { name: "B", line: "10", file: "@boo" },
line: 34,
types: [{
mirType: uniqStr("Int32"),
@ -252,12 +254,12 @@ gThread.frameTable.data.forEach((frame) => {
frame[LOCATION_SLOT] = uniqStr("A (http://foo/bar/baz:12)");
frame[OPTIMIZATIONS_SLOT] = gRawSite1;
break;
case "A_O2":
frame[LOCATION_SLOT] = uniqStr("A (http://foo/bar/baz:12)");
case "B_O2":
frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
frame[OPTIMIZATIONS_SLOT] = gRawSite2;
break;
case "B_O3":
frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:34)");
frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
frame[OPTIMIZATIONS_SLOT] = gRawSite3;
break;
}

View File

@ -12,6 +12,7 @@ let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
let { console } = devtools.require("resource://gre/modules/devtools/Console.jsm");
let { merge } = devtools.require("sdk/util/object");
let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
let { getPerformanceFront, PerformanceFront } = devtools.require("devtools/performance/front");

View File

@ -6,6 +6,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
let { console } = devtools.require("resource://gre/modules/devtools/Console.jsm");
const RecordingUtils = devtools.require("devtools/performance/recording-utils");
const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";

View File

@ -3,17 +3,57 @@
/**
* Tests that when constructing FrameNodes, if optimization data is available,
* the FrameNodes have the correct optimization data after iterating over samples.
* the FrameNodes have the correct optimization data after iterating over samples,
* and only youngest frames capture optimization data.
*/
function run_test() {
run_next_test();
}
add_task(function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 0, endTime: 30 }), "(root)");
let A = getFrameNodePath(root, "A");
let B = getFrameNodePath(A, "B");
let C = getFrameNodePath(B, "C");
let Aopts = A.getOptimizations();
let Bopts = B.getOptimizations();
let Copts = C.getOptimizations();
ok(!Aopts, "A() was never youngest frame, so should not have optimization data");
equal(Bopts.length, 2, "B() only has optimization data when it was a youngest frame");
// Check a few properties on the OptimizationSites.
let optSitesObserved = new Set();
for (let opt of Bopts) {
if (opt.data.line === 12) {
equal(opt.samples, 2, "Correct amount of samples for B()'s first opt site");
equal(opt.data.attempts.length, 3, "First opt site has 3 attempts");
equal(opt.data.attempts[0].strategy, "SomeGetter1", "inflated strategy name");
equal(opt.data.attempts[0].outcome, "Failure1", "inflated outcome name");
equal(opt.data.types[0].typeset[0].keyedBy, "constructor", "inflates type info");
optSitesObserved.add("first");
} else {
equal(opt.samples, 1, "Correct amount of samples for B()'s second opt site");
optSitesObserved.add("second");
}
}
ok(optSitesObserved.has("first"), "first opt site for B() was checked");
ok(optSitesObserved.has("second"), "second opt site for B() was checked");
equal(Copts.length, 1, "C() always youngest frame, so has optimization data");
});
let gUniqueStacks = new RecordingUtils.UniqueStacks();
function uniqStr(s) {
return gUniqueStacks.getOrAddStringIndex(s);
}
let time = 1;
let gThread = RecordingUtils.deflateThread({
samples: [{
time: 0,
@ -21,52 +61,48 @@ let gThread = RecordingUtils.deflateThread({
{ location: "(root)" }
]
}, {
time: time++,
frames: [
{ location: "(root)" },
{ location: "A_O1" },
{ location: "B" },
{ location: "C" }
]
}, {
time: time++,
frames: [
{ location: "(root)" },
{ location: "A_O1" },
{ location: "D" },
{ location: "C" }
]
}, {
time: time++,
frames: [
{ location: "(root)" },
{ location: "A_O2" },
{ location: "E_O3" },
{ location: "C" }
],
}, {
time: time++,
time: 10,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "B" },
{ location: "F" }
{ location: "B_LEAF_1" }
]
}, {
time: 15,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "B_NOTLEAF" },
{ location: "C" },
]
}, {
time: 20,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "B_LEAF_2" }
]
}, {
time: 25,
frames: [
{ location: "(root)" },
{ location: "A" },
{ location: "B_LEAF_2" }
]
}],
markers: []
}, gUniqueStacks);
// 3 OptimizationSites
let gRawSite1 = {
line: 12,
column: 2,
types: [{
mirType: uniqStr("Object"),
site: uniqStr("A (http://foo/bar/bar:12)"),
site: uniqStr("B (http://foo/bar:10)"),
typeset: [{
keyedBy: uniqStr("constructor"),
name: uniqStr("Foo"),
location: uniqStr("A (http://foo/bar/baz:12)")
location: uniqStr("B (http://foo/bar:10)")
}, {
keyedBy: uniqStr("primitive"),
location: uniqStr("self-hosted")
@ -86,7 +122,7 @@ let gRawSite1 = {
};
let gRawSite2 = {
line: 34,
line: 22,
types: [{
mirType: uniqStr("Int32"),
site: uniqStr("Receiver")
@ -104,32 +140,9 @@ let gRawSite2 = {
}
};
let gRawSite3 = {
line: 78,
types: [{
mirType: uniqStr("Object"),
site: uniqStr("A (http://foo/bar/bar:12)"),
typeset: [{
keyedBy: uniqStr("constructor"),
name: uniqStr("Foo"),
location: uniqStr("A (http://foo/bar/baz:12)")
}, {
keyedBy: uniqStr("primitive"),
location: uniqStr("self-hosted")
}]
}],
attempts: {
schema: {
outcome: 0,
strategy: 1
},
data: [
[uniqStr("Failure1"), uniqStr("SomeGetter1")],
[uniqStr("Failure2"), uniqStr("SomeGetter2")],
[uniqStr("GenericSuccess"), uniqStr("SomeGetter3")]
]
}
};
function serialize (x) {
return JSON.parse(JSON.stringify(x));
}
gThread.frameTable.data.forEach((frame) => {
const LOCATION_SLOT = gThread.frameTable.schema.location;
@ -137,45 +150,25 @@ gThread.frameTable.data.forEach((frame) => {
let l = gThread.stringTable[frame[LOCATION_SLOT]];
switch (l) {
case "A_O1":
frame[LOCATION_SLOT] = uniqStr("A");
frame[OPTIMIZATIONS_SLOT] = gRawSite1;
case "A":
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
break;
case "A_O2":
frame[LOCATION_SLOT] = uniqStr("A");
frame[OPTIMIZATIONS_SLOT] = gRawSite2;
// Rename some of the location sites so we can register different
// frames with different opt sites
case "B_LEAF_1":
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite2);
frame[LOCATION_SLOT] = uniqStr("B");
break;
case "E_O3":
frame[LOCATION_SLOT] = uniqStr("E");
frame[OPTIMIZATIONS_SLOT] = gRawSite3;
case "B_LEAF_2":
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
frame[LOCATION_SLOT] = uniqStr("B");
break;
case "B_NOTLEAF":
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
frame[LOCATION_SLOT] = uniqStr("B");
break;
case "C":
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
break;
}
});
function run_test() {
run_next_test();
}
add_task(function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let root = new ThreadNode(gThread, { startTime: 0, endTime: 4 });
let A = getFrameNodePath(root, "(root) > A");
let opts = A.getOptimizations();
let sites = opts.optimizationSites;
equal(sites.length, 2, "Frame A has two optimization sites.");
equal(sites[0].samples, 2, "first opt site has 2 samples.");
equal(sites[1].samples, 1, "second opt site has 1 sample.");
let E = getFrameNodePath(A, "E");
opts = E.getOptimizations();
sites = opts.optimizationSites;
equal(sites.length, 1, "Frame E has one optimization site.");
equal(sites[0].samples, 1, "first opt site has 1 samples.");
let D = getFrameNodePath(A, "D");
ok(!D.getOptimizations(),
"frames that do not have any opts data do not have JITOptimizations instances.");
});

View File

@ -111,15 +111,17 @@ diff --git a/browser/devtools/sourceeditor/codemirror/search/search.js b/browser
+ if (!queryDialog) {
+ let doc = cm.getWrapperElement().ownerDocument;
+ let inp = doc.createElement("input");
+ let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage"));
+
+ inp.type = "text";
+ inp.style.width = "10em";
+ inp.type = "search";
+ inp.placeholder = cm.l10n("findCmd.promptMessage");
+ inp.style.MozMarginStart = "1em";
+ inp.style.MozMarginEnd = "1em";
+ inp.style.flexGrow = "1";
+ inp.addEventListener("focus", () => inp.select());
+
+ queryDialog = doc.createElement("div");
+ queryDialog.appendChild(txt);
+ queryDialog.appendChild(inp);
+ queryDialog.style.display = "flex";
+ }
var state = getSearchState(cm);
if (state.query) return findNext(cm, rev);

View File

@ -75,15 +75,17 @@
if (!queryDialog) {
let doc = cm.getWrapperElement().ownerDocument;
let inp = doc.createElement("input");
let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage"));
inp.type = "text";
inp.style.width = "10em";
inp.type = "search";
inp.placeholder = cm.l10n("findCmd.promptMessage");
inp.style.MozMarginStart = "1em";
inp.style.MozMarginEnd = "1em";
inp.style.flexGrow = "1";
inp.addEventListener("focus", () => inp.select());
queryDialog = doc.createElement("div");
queryDialog.appendChild(txt);
queryDialog.appendChild(inp);
queryDialog.style.display = "flex";
}
var state = getSearchState(cm);
if (state.query) return findNext(cm, rev);

View File

@ -1,16 +1,67 @@
/* 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/. */
* file, You can obtain one at http://mozilla.org/MPL/2.0/ */
.theme-sidebar {
box-sizing: border-box;
}
body {
/* The view will grow bigger as the window gets resized, until 400px */
max-width: 400px;
margin: 0px auto;
padding: 0;
/* "Contain" the absolutely positioned #main element */
position: relative;
}
/* Header: contains the position and size of the element */
#header {
box-sizing: border-box;
width: 100%;
padding: 4px 14px;
display: -moz-box;
vertical-align: top;
}
#header:-moz-dir(rtl) {
-moz-box-direction: reverse;
}
#header > span {
display: -moz-box;
}
#element-size {
-moz-box-flex: 1;
}
#element-size:-moz-dir(rtl) {
-moz-box-pack: end;
}
@media (max-height: 228px) {
#header {
padding-top: 0;
padding-bottom: 0;
margin-top: 10px;
margin-bottom: 8px;
}
}
/* Main: contains the box-model regions */
#main {
position: absolute;
box-sizing: border-box;
/* The regions are semi-transparent, so the white background is partly
visible */
background-color: white;
border-color: hsla(210,100%,85%,0.7);
border-style: dotted;
color: var(--theme-selection-color);
/* Make sure there is some space between the window's edges and the regions */
margin: 0 14px 10px 14px;
width: calc(100% - 2 * 14px);
}
.margin,
@ -18,43 +69,271 @@
color: var(--theme-highlight-blue);
}
/* Regions are 3 nested elements with wide borders and outlines */
#content {
background-color: #87ceeb;
border-color: hsl(210,100%,85%);
border-style: dotted;
height: 25px;
}
#padding,
#margins {
#margins,
#borders,
#padding {
border-color: hsla(210,100%,85%,0.2);
border-width: 25px;
border-style: solid;
outline: dotted 1px hsl(210,100%,85%);
}
#padding {
background-color: #6a5acd;
}
#borders {
background-color: #444444;
border-style: dotted;
border-color: hsl(210,100%,85%);
}
#margins {
background-color: #edff64;
/* This opacity applies to all of the regions, since they are nested. */
/* This opacity applies to all of the regions, since they are nested */
opacity: .8;
}
/* Respond to window size change by changing the size of the regions */
@media (max-height: 228px) {
#content {
height: 18px;
}
#margins,
#borders,
#padding {
border-width: 18px;
}
}
/* Regions colors */
#margins {
border-color: #edff64;
}
#borders {
border-color: #444444;
}
#padding {
border-color: #6a5acd;
}
#content {
background-color: #87ceeb;
}
/* Editable region sizes are contained in absolutely positioned <p> */
#main > p {
position: absolute;
pointer-events: none;
}
#main > p {
margin: 0;
text-align: center;
}
#main > p > span {
vertical-align: middle;
pointer-events: auto;
}
/* Coordinates for the region sizes */
.top,
.bottom {
width: calc(100% - 2px);
text-align: center;
}
.padding.top {
top: 55px;
}
.padding.bottom {
bottom: 57px;
}
.border.top {
top: 30px;
}
.border.bottom {
bottom: 31px;
}
.margin.top {
top: 5px;
}
.margin.bottom {
bottom: 6px;
}
.size,
.margin.left,
.margin.right,
.border.left,
.border.right,
.padding.left,
.padding.right {
top: 22px;
line-height: 132px;
}
.size {
width: calc(100% - 2px);
}
.margin.right,
.margin.left,
.border.left,
.border.right,
.padding.right,
.padding.left {
width: 25px;
}
.padding.left {
left: 52px;
}
.padding.right {
right: 51px;
}
.border.left {
left: 26px;
}
.border.right {
right: 26px;
}
.margin.right {
right: 0;
}
.margin.left {
left: 0;
}
.rotate.left:not(.editing) {
transform: rotate(-90deg);
}
.rotate.right:not(.editing) {
transform: rotate(90deg);
}
/* Coordinates should be different when the window is small, because we make
the regions smaller then */
@media (max-height: 228px) {
.padding.top {
top: 37px;
}
.padding.bottom {
bottom: 38px;
}
.border.top {
top: 19px;
}
.border.bottom {
bottom: 20px;
}
.margin.top {
top: 1px;
}
.margin.bottom {
bottom: 2px;
}
.size,
.margin.left,
.margin.right,
.border.left,
.border.right,
.padding.left,
.padding.right {
line-height: 80px;
}
.margin.right,
.margin.left,
.border.left,
.border.right,
.padding.right,
.padding.left {
width: 21px;
}
.padding.left {
left: 35px;
}
.padding.right {
right: 35px;
}
.border.left {
left: 16px;
}
.border.right {
right: 17px;
}
}
/* Legend, displayed inside regions */
.legend {
position: absolute;
margin: 5px 6px;
z-index: 1;
}
.legend[data-box="margin"] {
color: var(--theme-highlight-blue);
}
@media (max-height: 228px) {
.legend {
margin: 2px 6px;
}
}
/* Editable fields */
.editable {
border: 1px dashed transparent;
-moz-user-select: text;
}
.editable:hover {
border-bottom-color: hsl(0,0%,50%);
border-bottom-color: hsl(0, 0%, 50%);
}
.styleinspector-propertyeditor {
border: 1px solid #CCC;
border: 1px solid #ccc;
padding: 0;
}
/* Make sure the content size doesn't appear as editable like the other sizes */
.size > span {
cursor: default;
}
/* Hide all values when the view is inactive */
body.inactive > #header > #element-position,
body.inactive > #header > #element-size,
body.inactive > #main > p {
visibility: hidden;
}

View File

@ -322,6 +322,7 @@ description > html|a {
#dialogTitle {
text-align: center;
-moz-user-select: none;
}
.close-icon {

View File

@ -1329,17 +1329,12 @@ void
_memfree(void* aPtr)
{
PLUGIN_LOG_DEBUG_FUNCTION;
// Only assert plugin thread here for consistency with in-process plugins.
AssertPluginThread();
free(aPtr);
}
uint32_t
_memflush(uint32_t aSize)
{
PLUGIN_LOG_DEBUG_FUNCTION;
// Only assert plugin thread here for consistency with in-process plugins.
AssertPluginThread();
return 0;
}
@ -1398,8 +1393,6 @@ void*
_memalloc(uint32_t aSize)
{
PLUGIN_LOG_DEBUG_FUNCTION;
// Only assert plugin thread here for consistency with in-process plugins.
AssertPluginThread();
return moz_xmalloc(aSize);
}

View File

@ -599,13 +599,9 @@ pref("browser.safebrowsing.debug", false);
pref("browser.safebrowsing.updateURL", "https://safebrowsing.google.com/safebrowsing/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2&key=%GOOGLE_API_KEY%");
pref("browser.safebrowsing.gethashURL", "https://safebrowsing.google.com/safebrowsing/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
pref("browser.safebrowsing.reportURL", "https://safebrowsing.google.com/safebrowsing/report?");
pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
pref("browser.safebrowsing.reportPhishMistakeURL", "https://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.reportPhishURL", "https://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.reportMalwareMistakeURL", "https://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%&url=");
pref("browser.safebrowsing.malware.reportURL", "https://safebrowsing.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
pref("browser.safebrowsing.id", @MOZ_APP_UA_NAME@);

View File

@ -6,7 +6,6 @@
package org.mozilla.gecko;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
@ -17,6 +16,7 @@ import org.mozilla.gecko.util.ThreadUtils;
import android.content.Context;
import android.hardware.input.InputManager;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
@ -129,8 +129,8 @@ public class AndroidGamepadManager {
}
private static boolean sStarted;
private static HashMap<Integer, Gamepad> sGamepads;
private static HashMap<Integer, List<KeyEvent>> sPendingGamepads;
private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
private static InputManager.InputDeviceListener sListener;
private static Timer sPollTimer;
@ -140,8 +140,6 @@ public class AndroidGamepadManager {
public static void startup() {
ThreadUtils.assertOnUiThread();
if (!sStarted) {
sGamepads = new HashMap<Integer, Gamepad>();
sPendingGamepads = new HashMap<Integer, List<KeyEvent>>();
scanForGamepads();
addDeviceListener();
sStarted = true;
@ -152,8 +150,8 @@ public class AndroidGamepadManager {
ThreadUtils.assertOnUiThread();
if (sStarted) {
removeDeviceListener();
sPendingGamepads = null;
sGamepads = null;
sPendingGamepads.clear();
sGamepads.clear();
sStarted = false;
}
}
@ -163,12 +161,13 @@ public class AndroidGamepadManager {
if (!sStarted) {
return;
}
if (!sPendingGamepads.containsKey(deviceId)) {
final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
if (pending == null) {
removeGamepad(deviceId);
return;
}
List<KeyEvent> pending = sPendingGamepads.get(deviceId);
sPendingGamepads.remove(deviceId);
sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
// Handle queued KeyEvents
@ -200,12 +199,12 @@ public class AndroidGamepadManager {
return false;
}
if (!sGamepads.containsKey(ev.getDeviceId())) {
final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
if (gamepad == null) {
// Not a device we care about.
return false;
}
Gamepad gamepad = sGamepads.get(ev.getDeviceId());
// First check the analog stick axes
boolean[] valid = new boolean[Axis.values().length];
float[] axes = new float[Axis.values().length];
@ -254,13 +253,14 @@ public class AndroidGamepadManager {
}
int deviceId = ev.getDeviceId();
if (sPendingGamepads.containsKey(deviceId)) {
final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
if (pendingGamepad != null) {
// Queue up key events for pending devices.
sPendingGamepads.get(deviceId).add(ev);
pendingGamepad.add(ev);
return true;
}
if (!sGamepads.containsKey(deviceId)) {
if (sGamepads.get(deviceId) == null) {
InputDevice device = ev.getDevice();
if (device != null &&
(device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
@ -336,7 +336,8 @@ public class AndroidGamepadManager {
sPollTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
for (Integer deviceId : sGamepads.keySet()) {
for (int i = 0; i < sGamepads.size(); ++i) {
final int deviceId = sGamepads.keyAt(i);
if (InputDevice.getDevice(deviceId) == null) {
removeGamepad(deviceId);
}
@ -359,13 +360,13 @@ public class AndroidGamepadManager {
@Override
public void onInputDeviceRemoved(int deviceId) {
if (sPendingGamepads.containsKey(deviceId)) {
if (sPendingGamepads.get(deviceId) != null) {
// Got removed before Gecko's ack reached us.
// gamepadAdded will deal with it.
sPendingGamepads.remove(deviceId);
return;
}
if (sGamepads.containsKey(deviceId)) {
if (sGamepads.get(deviceId) != null) {
removeGamepad(deviceId);
}
}

View File

@ -16,8 +16,12 @@ import org.json.JSONObject;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
@ -27,8 +31,14 @@ public final class IntentHelper implements GeckoEventListener {
"Intent:GetHandlers",
"Intent:Open",
"Intent:OpenForResult",
"Intent:OpenNoHandler",
"WebActivity:Open"
};
// via http://developer.android.com/distribute/tools/promote/linking.html
private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
private static IntentHelper instance;
private final Activity activity;
@ -64,6 +74,8 @@ public final class IntentHelper implements GeckoEventListener {
open(message);
} else if (event.equals("Intent:OpenForResult")) {
openForResult(message);
} else if (event.equals("Intent:OpenNoHandler")) {
openNoHandler(message);
} else if (event.equals("WebActivity:Open")) {
openWebActivity(message);
}
@ -111,6 +123,66 @@ public final class IntentHelper implements GeckoEventListener {
}
}
/**
* Opens a URI without any valid handlers on device. In the best case, a package is specified
* and we can bring the user directly to the application page in an app market. If a package is
* not specified and there is a fallback url in the intent extras, we open that url. If neither
* is present, we alert the user that we were unable to open the link.
*/
private void openNoHandler(final JSONObject msg) {
final String uri = msg.optString("uri");
if (TextUtils.isEmpty(uri)) {
displayToastCannotOpenLink();
Log.w(LOGTAG, "Received empty URL. Ignoring...");
return;
}
final Intent intent;
try {
// TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices.
intent = Intent.parseUri(uri, 0);
} catch (final URISyntaxException e) {
displayToastCannotOpenLink();
// Don't log the exception to prevent leaking URIs.
Log.w(LOGTAG, "Unable to parse Intent URI");
return;
}
// For this flow, we follow Chrome's lead:
// https://developer.chrome.com/multidevice/android/intents
//
// Note on alternative flows: we could get the intent package from a component, however, for
// security reasons, components are ignored when opening URIs (bug 1168998) so we should
// ignore it here too.
//
// Our old flow used to prompt the user to search for their app in the market by scheme and
// while this could help the user find a new app, there is not always a correlation in
// scheme to application name and we could end up steering the user wrong (potentially to
// malicious software). Better to leave that one alone.
if (intent.getPackage() != null) {
final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage();
final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
marketIntent.addCategory(Intent.CATEGORY_BROWSABLE);
marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(marketIntent);
} else if (intent.hasExtra(EXTRA_BROWSER_FALLBACK_URL)) {
final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
Tabs.getInstance().loadUrl(fallbackUrl);
} else {
displayToastCannotOpenLink();
// Don't log the URI to prevent leaking it.
Log.w(LOGTAG, "Unable to handle URI");
}
}
private void displayToastCannotOpenLink() {
final String errText = activity.getResources().getString(R.string.intent_uri_cannot_open);
Toast.makeText(activity, errText, Toast.LENGTH_LONG).show();
}
private void openWebActivity(JSONObject message) throws JSONException {
final Intent intent = WebActivityMapper.getIntentForWebActivity(message.getJSONObject("activity"));
ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));

View File

@ -177,7 +177,6 @@ public class SiteIdentity {
mVerifier = identityData.getString("verifier");
mEncrypted = identityData.optBoolean("encrypted", false);
} catch (Exception e) {
Log.e(LOGTAG, "Error fetching Site identity host info", e);
resetIdentity();
}
} catch (Exception e) {

View File

@ -659,3 +659,5 @@ just addresses the organization to follow, e.g. "This site is run by " -->
<!-- LOCALIZATION NOTE (find_matchcase): This is meant to appear as an icon that changes color
if match-case is activated. i.e. No more than two letters, one uppercase, one lowercase. -->
<!ENTITY find_matchcase "Aa">
<!ENTITY intent_uri_cannot_open "Cannot open link">

View File

@ -538,4 +538,6 @@
<string name="percent">&percent;</string>
<string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
<string name="intent_uri_cannot_open">&intent_uri_cannot_open;</string>
</resources>

View File

@ -49,26 +49,20 @@ ContentDispatchChooser.prototype =
if (aHandler.possibleApplicationHandlers.length > 1) {
aHandler.launchWithURI(aURI, aWindowContext);
} else {
// xpcshell tests do not have an Android Bridge but we require Android
// Bridge when using Messaging so we guard against this case. xpcshell
// tests also do not have a window, so we use this state to guard.
let win = this._getChromeWin();
if (win && win.NativeWindow) {
let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
let failedText = bundle.GetStringFromName("protocol.failed");
let searchText = bundle.GetStringFromName("protocol.toast.search");
win.NativeWindow.toast.show(failedText, "long", {
button: {
label: searchText,
callback: function() {
let message = {
type: "Intent:Open",
url: "market://search?q=" + aURI.scheme,
};
Messaging.sendRequest(message);
}
}
});
if (!win) {
return;
}
let msg = {
type: "Intent:OpenNoHandler",
uri: aURI.spec,
};
Messaging.sendRequest(msg);
}
},
};

View File

@ -3,6 +3,3 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
download.blocked=Unable to download file
protocol.failed=Couldn't find an app to open this link
# A very short string shown in the button toast when no application can open the url
protocol.toast.search=Search

View File

@ -863,8 +863,19 @@ LoginManagerPrompter.prototype = {
updateButtonLabel();
};
let onPasswordFocus = () => {
chromeDoc.getElementById("password-notification-password").type = "";
let onPasswordFocus = (focusEvent) => {
let passwordField = chromeDoc.getElementById("password-notification-password");
// Gets the caret position before changing the type of the textbox
let selectionStart = passwordField.selectionStart;
let selectionEnd = passwordField.selectionEnd;
if (focusEvent.rangeParent != null) {
// Check for a click over the SHOW placeholder
selectionStart = passwordField.value.length;
selectionEnd = passwordField.value.length;
}
passwordField.type = "";
passwordField.selectionStart = selectionStart;
passwordField.selectionEnd = selectionEnd;
};
let onPasswordBlur = () => {

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>engine-suggestions.xml</ShortName>
<Url type="application/x-suggestions+json"
method="GET"
template="http://localhost:9000/suggest?{searchTerms}"/>
<Url type="text/html"
method="GET"
template="http://localhost:9000/search"
rel="searchform"/>
</SearchPlugin>

View File

@ -8,6 +8,7 @@ const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/httpd.js");
// Import common head.
{
@ -29,7 +30,14 @@ function* cleanup() {
Services.prefs.clearUserPref("browser.urlbar.autoFill");
Services.prefs.clearUserPref("browser.urlbar.autoFill.typed");
Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
for (let type of ["history", "bookmark", "history.onlyTyped", "openpage"]) {
let suggestPrefs = [
"history",
"bookmark",
"history.onlyTyped",
"openpage",
"searches",
];
for (let type of suggestPrefs) {
Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
}
yield PlacesUtils.bookmarks.eraseEverything();
@ -222,7 +230,7 @@ function* check_autocomplete(test) {
// We didn't hit the break, so we must have not found it
if (j == matches.length)
do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`);
do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`); //' (Emacs syntax highlighting fix)
}
Assert.equal(controller.matchCount, matches.length,
@ -393,12 +401,56 @@ function setFaviconForHref(href, iconHref) {
});
}
function makeTestServer(port=-1) {
let httpServer = new HttpServer();
httpServer.start(port);
do_register_cleanup(() => httpServer.stop(() => {}));
return httpServer;
}
function* addTestEngine(basename, httpServer=undefined) {
httpServer = httpServer || makeTestServer();
httpServer.registerDirectory("/", do_get_cwd());
let dataUrl =
"http://localhost:" + httpServer.identity.primaryPort + "/data/";
do_print("Adding engine: " + basename);
return yield new Promise(resolve => {
Services.obs.addObserver(function obs(subject, topic, data) {
let engine = subject.QueryInterface(Ci.nsISearchEngine);
do_print("Observed " + data + " for " + engine.name);
if (data != "engine-added" || engine.name != basename) {
return;
}
Services.obs.removeObserver(obs, "browser-search-engine-modified");
do_register_cleanup(() => Services.search.removeEngine(engine));
resolve(engine);
}, "browser-search-engine-modified", false);
do_print("Adding engine from URL: " + dataUrl + basename);
Services.search.addEngine(dataUrl + basename,
Ci.nsISearchEngine.DATA_XML, null, false);
});
}
// Ensure we have a default search engine and the keyword.enabled preference
// set.
add_task(function ensure_search_engine() {
// keyword.enabled is necessary for the tests to see keyword searches.
Services.prefs.setBoolPref("keyword.enabled", true);
// Initialize the search service, but first set this geo IP pref to a dummy
// string. When the search service is initialized, it contacts the URI named
// in this pref, which breaks the test since outside connections aren't
// allowed.
let geoPref = "browser.search.geoip.url";
Services.prefs.setCharPref(geoPref, "");
do_register_cleanup(() => Services.prefs.clearUserPref(geoPref));
yield new Promise(resolve => {
Services.search.init(resolve);
});
// Remove any existing engines before adding ours.
for (let engine of Services.search.getEngines()) {
Services.search.removeEngine(engine);

View File

@ -1,42 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://testing-common/httpd.js");
function* addTestEngines(items) {
let httpServer = new HttpServer();
httpServer.start(-1);
httpServer.registerDirectory("/", do_get_cwd());
let gDataUrl = "http://localhost:" + httpServer.identity.primaryPort + "/data/";
do_register_cleanup(() => httpServer.stop(() => {}));
let engines = [];
for (let item of items) {
do_print("Adding engine: " + item);
yield new Promise(resolve => {
Services.obs.addObserver(function obs(subject, topic, data) {
let engine = subject.QueryInterface(Ci.nsISearchEngine);
do_print("Observed " + data + " for " + engine.name);
if (data != "engine-added" || engine.name != item) {
return;
}
Services.obs.removeObserver(obs, "browser-search-engine-modified");
engines.push(engine);
resolve();
}, "browser-search-engine-modified", false);
do_print("`Adding engine from URL: " + gDataUrl + item);
Services.search.addEngine(gDataUrl + item,
Ci.nsISearchEngine.DATA_XML, null, false);
});
}
return engines;
}
add_task(function* test_searchEngine_autoFill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("MySearchEngine", "", "", "",
@ -67,8 +31,7 @@ add_task(function* test_searchEngine_autoFill() {
add_task(function* test_searchEngine_noautoFill() {
let engineName = "engine-rel-searchform.xml";
let [engine] = yield addTestEngines([engineName]);
do_register_cleanup(() => Services.search.removeEngine(engine));
let engine = yield addTestEngine(engineName);
equal(engine.searchForm, "http://example.com/?search");
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);

View File

@ -0,0 +1,147 @@
Cu.import("resource://gre/modules/FormHistory.jsm");
const ENGINE_NAME = "engine-suggestions.xml";
const SERVER_PORT = 9000;
const SUGGEST_PREF = "browser.urlbar.suggest.searches";
// Set this to some other function to change how the server converts search
// strings into suggestions.
let suggestionsFromSearchString = searchStr => {
let suffixes = ["foo", "bar"];
return suffixes.map(s => searchStr + " " + s);
};
add_task(function* setUp() {
// Set up a server that provides some suggestions by appending strings onto
// the search query.
let server = makeTestServer(SERVER_PORT);
server.registerPathHandler("/suggest", (req, resp) => {
// URL query params are x-www-form-urlencoded, which converts spaces into
// plus signs, so un-convert any plus signs back to spaces.
let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
let suggestions = suggestionsFromSearchString(searchStr);
let data = [searchStr, suggestions];
resp.setHeader("Content-Type", "application/json", false);
resp.write(JSON.stringify(data));
});
// Install the test engine.
let oldCurrentEngine = Services.search.currentEngine;
do_register_cleanup(() => Services.search.currentEngine = oldCurrentEngine);
let engine = yield addTestEngine(ENGINE_NAME, server);
Services.search.currentEngine = engine;
yield cleanup();
});
add_task(function* disabled() {
Services.prefs.setBoolPref(SUGGEST_PREF, false);
yield check_autocomplete({
search: "hello",
matches: [],
});
yield cleanup();
});
add_task(function* singleWordQuery() {
Services.prefs.setBoolPref(SUGGEST_PREF, true);
let searchStr = "hello";
yield check_autocomplete({
search: searchStr,
matches: [{
uri: makeActionURI(("searchengine"), {
engineName: ENGINE_NAME,
input: searchStr,
searchQuery: searchStr,
searchSuggestion: "hello foo",
}),
title: ENGINE_NAME,
style: ["action", "searchengine"],
icon: "",
}, {
uri: makeActionURI(("searchengine"), {
engineName: ENGINE_NAME,
input: searchStr,
searchQuery: searchStr,
searchSuggestion: "hello bar",
}),
title: ENGINE_NAME,
style: ["action", "searchengine"],
icon: "",
}],
});
yield cleanup();
});
add_task(function* multiWordQuery() {
Services.prefs.setBoolPref(SUGGEST_PREF, true);
let searchStr = "hello world";
yield check_autocomplete({
search: searchStr,
matches: [{
uri: makeActionURI(("searchengine"), {
engineName: ENGINE_NAME,
input: searchStr,
searchQuery: searchStr,
searchSuggestion: "hello world foo",
}),
title: ENGINE_NAME,
style: ["action", "searchengine"],
icon: "",
}, {
uri: makeActionURI(("searchengine"), {
engineName: ENGINE_NAME,
input: searchStr,
searchQuery: searchStr,
searchSuggestion: "hello world bar",
}),
title: ENGINE_NAME,
style: ["action", "searchengine"],
icon: "",
}],
});
yield cleanup();
});
add_task(function* suffixMatch() {
Services.prefs.setBoolPref(SUGGEST_PREF, true);
let oldFn = suggestionsFromSearchString;
suggestionsFromSearchString = searchStr => {
let prefixes = ["baz", "quux"];
return prefixes.map(p => p + " " + searchStr);
};
let searchStr = "hello";
yield check_autocomplete({
search: searchStr,
matches: [{
uri: makeActionURI(("searchengine"), {
engineName: ENGINE_NAME,
input: searchStr,
searchQuery: searchStr,
searchSuggestion: "baz hello",
}),
title: ENGINE_NAME,
style: ["action", "searchengine"],
icon: "",
}, {
uri: makeActionURI(("searchengine"), {
engineName: ENGINE_NAME,
input: searchStr,
searchQuery: searchStr,
searchSuggestion: "quux hello",
}),
title: ENGINE_NAME,
style: ["action", "searchengine"],
icon: "",
}],
});
suggestionsFromSearchString = oldFn;
yield cleanup();
});

View File

@ -4,7 +4,7 @@ tail =
skip-if = toolkit == 'android' || toolkit == 'gonk'
support-files =
data/engine-rel-searchform.xml
data/engine-suggestions.xml
[test_416211.js]
[test_416214.js]
@ -34,6 +34,7 @@ support-files =
[test_searchEngine_current.js]
[test_searchEngine_host.js]
[test_searchEngine_restyle.js]
[test_searchSuggestions.js]
[test_special_search.js]
[test_swap_protocol.js]
[test_tabmatches.js]

View File

@ -90,15 +90,36 @@ this.SafeBrowsing = {
gethashURL: null,
reportURL: null,
reportGenericURL: null,
reportErrorURL: null,
reportPhishURL: null,
reportMalwareURL: null,
reportMalwareErrorURL: null,
getReportURL: function(kind, URI) {
let pref;
switch (kind) {
case "Phish":
pref = "browser.safebrowsing.reportPhishURL";
break;
case "PhishMistake":
pref = "browser.safebrowsing.reportPhishMistakeURL";
break;
case "MalwareMistake":
pref = "browser.safebrowsing.reportMalwareMistakeURL";
break;
getReportURL: function(kind) {
return this["report" + kind + "URL"];
default:
let err = "SafeBrowsing getReportURL() called with unknown kind: " + kind;
Components.utils.reportError(err);
throw err;
}
let reportUrl = Services.urlFormatter.formatURLPref(pref);
let pageUri = URI.clone();
// Remove the query to avoid including potentially sensitive data
if (pageUri instanceof Ci.nsIURL)
pageUri.query = '';
reportUrl += encodeURIComponent(pageUri.asciiSpec);
return reportUrl;
},
@ -128,19 +149,10 @@ this.SafeBrowsing = {
}
log("initializing safe browsing URLs, client id ", clientID);
let basePref = "browser.safebrowsing.";
// Urls to HTML report pages
this.reportURL = Services.urlFormatter.formatURLPref(basePref + "reportURL");
this.reportGenericURL = Services.urlFormatter.formatURLPref(basePref + "reportGenericURL");
this.reportErrorURL = Services.urlFormatter.formatURLPref(basePref + "reportErrorURL");
this.reportPhishURL = Services.urlFormatter.formatURLPref(basePref + "reportPhishURL");
this.reportMalwareURL = Services.urlFormatter.formatURLPref(basePref + "reportMalwareURL");
this.reportMalwareErrorURL = Services.urlFormatter.formatURLPref(basePref + "reportMalwareErrorURL");
// Urls used to update DB
this.updateURL = Services.urlFormatter.formatURLPref(basePref + "updateURL");
this.gethashURL = Services.urlFormatter.formatURLPref(basePref + "gethashURL");
this.updateURL = Services.urlFormatter.formatURLPref("browser.safebrowsing.updateURL");
this.gethashURL = Services.urlFormatter.formatURLPref("browser.safebrowsing.gethashURL");
this.updateURL = this.updateURL.replace("SAFEBROWSING_ID", clientID);
this.gethashURL = this.gethashURL.replace("SAFEBROWSING_ID", clientID);

View File

@ -1360,7 +1360,7 @@ TabClient.prototype = {
eventSource(TabClient.prototype);
function WorkerClient(aClient, aForm) {
this._client = aClient;
this.client = aClient;
this._actor = aForm.from;
this._isClosed = false;
this._isFrozen = aForm.isFrozen;
@ -1376,11 +1376,11 @@ function WorkerClient(aClient, aForm) {
WorkerClient.prototype = {
get _transport() {
return this._client._transport;
return this.client._transport;
},
get request() {
return this._client.request;
return this.client.request;
},
get actor() {
@ -1397,19 +1397,41 @@ WorkerClient.prototype = {
detach: DebuggerClient.requester({ type: "detach" }, {
after: function (aResponse) {
this._client.unregisterClient(this);
this.client.unregisterClient(this);
return aResponse;
},
telemetry: "WORKERDETACH"
}),
attachThread: function(aOptions = {}, aOnResponse = noop) {
if (this.thread) {
DevToolsUtils.executeSoon(() => aOnResponse({
type: "connected",
threadActor: this.thread._actor,
}, this.thread));
return;
}
this.request({
to: this._actor,
type: "connect",
options: aOptions,
}, (aResponse) => {
if (!aResponse.error) {
this.thread = new ThreadClient(this, aResponse.threadActor);
this.client.registerClient(this.thread);
}
aOnResponse(aResponse, this.thread);
});
},
_onClose: function () {
this.removeListener("close", this._onClose);
this.removeListener("freeze", this._onFreeze);
this.removeListener("thaw", this._onThaw);
this._client.unregisterClient(this);
this.client.unregisterClient(this);
this._closed = true;
},

View File

@ -15,6 +15,7 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, dumpn, update, fetch } = DevToolsUtils;
const { dirname, joinURI } = require("devtools/toolkit/path");
const promise = require("promise");
const PromiseDebugging = require("PromiseDebugging");
const xpcInspector = require("xpcInspector");
const ScriptStore = require("./utils/ScriptStore");
const {DevToolsWorker} = require("devtools/toolkit/shared/worker.js");
@ -1494,7 +1495,7 @@ ThreadActor.prototype = {
// Clear DOM event breakpoints.
// XPCShell tests don't use actual DOM windows for globals and cause
// removeListenerForAllEvents to throw.
if (this.global && !this.global.toString().includes("Sandbox")) {
if (!isWorker && this.global && !this.global.toString().includes("Sandbox")) {
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
@ -1933,7 +1934,7 @@ ThreadActor.prototype = {
}
if (promises.length > 0) {
this.synchronize(Promise.all(promises));
this.synchronize(promise.all(promises));
}
return true;
@ -2870,10 +2871,10 @@ SourceActor.prototype = {
actor,
GeneratedLocation.fromOriginalLocation(originalLocation)
)) {
return Promise.resolve(null);
return promise.resolve(null);
}
return Promise.resolve(originalLocation);
return promise.resolve(originalLocation);
} else {
return this.sources.getAllGeneratedLocations(originalLocation)
.then((generatedLocations) => {

View File

@ -10,7 +10,7 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, fetch } = DevToolsUtils;
const EventEmitter = require("devtools/toolkit/event-emitter");
const { OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common");
const { resolve } = Promise;
const { resolve } = require("promise");
loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/script", true);

View File

@ -1,6 +1,7 @@
"use strict";
let { Ci, Cu } = require("chrome");
let { DebuggerServer } = require("devtools/server/main");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -28,6 +29,8 @@ function matchWorkerDebugger(dbg, options) {
function WorkerActor(dbg) {
this._dbg = dbg;
this._isAttached = false;
this._threadActor = null;
this._transport = null;
}
WorkerActor.prototype = {
@ -66,6 +69,33 @@ WorkerActor.prototype = {
return { type: "detached" };
},
onConnect: function (request) {
if (!this._isAttached) {
return { error: "wrongState" };
}
if (this._threadActor !== null) {
return {
type: "connected",
threadActor: this._threadActor
};
}
return DebuggerServer.connectToWorker(
this.conn, this._dbg, this.actorID, request.options
).then(({ threadActor, transport }) => {
this._threadActor = threadActor;
this._transport = transport;
return {
type: "connected",
threadActor: this._threadActor
};
}, (error) => {
return { error: error.toString() };
});
},
onClose: function () {
if (this._isAttached) {
this._detach();
@ -74,6 +104,10 @@ WorkerActor.prototype = {
this.conn.sendActorEvent(this.actorID, "close");
},
onError: function (filename, lineno, message) {
reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n");
},
onFreeze: function () {
this.conn.sendActorEvent(this.actorID, "freeze");
},
@ -83,6 +117,12 @@ WorkerActor.prototype = {
},
_detach: function () {
if (this._threadActor !== null) {
this._transport.close();
this._transport = null;
this._threadActor = null;
}
this._dbg.removeListener(this);
this._isAttached = false;
}
@ -90,7 +130,8 @@ WorkerActor.prototype = {
WorkerActor.prototype.requestTypes = {
"attach": WorkerActor.prototype.onAttach,
"detach": WorkerActor.prototype.onDetach
"detach": WorkerActor.prototype.onDetach,
"connect": WorkerActor.prototype.onConnect
};
exports.WorkerActor = WorkerActor;

View File

@ -14,7 +14,7 @@ let { Ci, Cc, CC, Cu, Cr } = require("chrome");
let Services = require("Services");
let { ActorPool, OriginalLocation, RegisteredActorFactory,
ObservedActorFactory } = require("devtools/server/actors/common");
let { LocalDebuggerTransport, ChildDebuggerTransport } =
let { LocalDebuggerTransport, ChildDebuggerTransport, WorkerDebuggerTransport } =
require("devtools/toolkit/transport/transport");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let { dumpn, dumpv, dbg_assert } = DevToolsUtils;
@ -685,10 +685,13 @@ var DebuggerServer = {
* "debug:<prefix>:packet", and all its actors will have names
* beginning with "<prefix>/".
*/
connectToParent: function(aPrefix, aMessageManager) {
connectToParent: function(aPrefix, aScopeOrManager) {
this._checkInit();
let transport = new ChildDebuggerTransport(aMessageManager, aPrefix);
let transport = isWorker ?
new WorkerDebuggerTransport(aScopeOrManager, aPrefix) :
new ChildDebuggerTransport(aScopeOrManager, aPrefix);
return this._onConnection(transport, aPrefix, true);
},
@ -755,6 +758,83 @@ var DebuggerServer = {
return deferred.promise;
},
connectToWorker: function (aConnection, aDbg, aId, aOptions) {
return new Promise((resolve, reject) => {
// Step 1: Initialize the worker debugger.
aDbg.initialize("resource://gre/modules/devtools/server/worker.js");
// Step 2: Send a connect request to the worker debugger.
aDbg.postMessage(JSON.stringify({
type: "connect",
id: aId,
options: aOptions
}));
// Steps 3-5 are performed on the worker thread (see worker.js).
// Step 6: Wait for a response from the worker debugger.
let listener = {
onClose: () => {
aDbg.removeListener(listener);
reject("closed");
},
onMessage: (message) => {
let packet = JSON.parse(message);
if (packet.type !== "message" || packet.id !== aId) {
return;
}
message = packet.message;
if (message.error) {
reject(error);
}
if (message.type !== "paused") {
return;
}
aDbg.removeListener(listener);
// Step 7: Create a transport for the connection to the worker.
let transport = new WorkerDebuggerTransport(aDbg, aId);
transport.ready();
transport.hooks = {
onClosed: () => {
if (!aDbg.isClosed) {
aDbg.postMessage(JSON.stringify({
type: "disconnect",
id: aId
}));
}
aConnection.cancelForwarding(aId);
},
onPacket: (packet) => {
// Ensure that any packets received from the server on the worker
// thread are forwarded to the client on the main thread, as if
// they had been sent by the server on the main thread.
aConnection.send(packet);
}
};
// Ensure that any packets received from the client on the main thread
// to actors on the worker thread are forwarded to the server on the
// worker thread.
aConnection.setForwarding(aId, transport);
resolve({
threadActor: message.from,
transport: transport
});
}
};
aDbg.addListener(listener);
});
},
/**
* Check if the caller is running in a content child process.
*
@ -1442,13 +1522,16 @@ DebuggerServerConnection.prototype = {
// forwarding is needed: in DebuggerServerConnection instances in child
// processes, every actor has a prefixed name.
if (this._forwardingPrefixes.size > 0) {
let separator = aPacket.to.indexOf('/');
if (separator >= 0) {
let to = aPacket.to;
let separator = to.lastIndexOf('/');
while (separator >= 0) {
to = to.substring(0, separator);
let forwardTo = this._forwardingPrefixes.get(aPacket.to.substring(0, separator));
if (forwardTo) {
forwardTo.send(aPacket);
return;
}
separator = to.lastIndexOf('/');
}
}

View File

@ -51,6 +51,7 @@ EXTRA_JS_MODULES.devtools.server += [
'content-globals.js',
'main.js',
'protocol.js',
'worker.js'
]
EXTRA_JS_MODULES.devtools.server.actors += [

View File

@ -0,0 +1,66 @@
"use strict"
loadSubScript("resource://gre/modules/devtools/worker-loader.js");
let { ActorPool } = worker.require("devtools/server/actors/common");
let { ThreadActor } = worker.require("devtools/server/actors/script");
let { TabSources } = worker.require("devtools/server/actors/utils/TabSources");
let makeDebugger = worker.require("devtools/server/actors/utils/make-debugger");
let { DebuggerServer } = worker.require("devtools/server/main");
DebuggerServer.init();
DebuggerServer.createRootActor = function () {
throw new Error("Should never get here!");
};
let connections = Object.create(null);
this.addEventListener("message", function (event) {
let packet = JSON.parse(event.data);
switch (packet.type) {
case "connect":
// Step 3: Create a connection to the parent.
let connection = DebuggerServer.connectToParent(packet.id, this);
connections[packet.id] = connection;
// Step 4: Create a thread actor for the connection to the parent.
let pool = new ActorPool(connection);
connection.addActorPool(pool);
let sources = null;
let actor = new ThreadActor({
makeDebugger: makeDebugger.bind(null, {
findDebuggees: () => {
return [this.global];
},
shouldAddNewGlobalAsDebuggee: () => {
return true;
},
}),
get sources() {
if (sources === null) {
sources = new TabSources(actor);
}
return sources;
}
}, global);
pool.addActor(actor);
// Step 5: Attach to the thread actor.
//
// This will cause a packet to be sent over the connection to the parent.
// Because this connection uses WorkerDebuggerTransport internally, this
// packet will be sent using WorkerDebuggerGlobalScope.postMessage, causing
// an onMessage event to be fired on the WorkerDebugger in the main thread.
actor.onAttach({});
break;
case "disconnect":
connections[packet.id].close();
break;
};
});

View File

@ -435,6 +435,8 @@ let {
} else { // Worker thread
let requestors = [];
let scope = this;
let xpcInspector = {
get lastNestRequestor() {
return requestors.length === 0 ? null : requestors[0];
@ -442,13 +444,13 @@ let {
enterNestedEventLoop: function (requestor) {
requestors.push(requestor);
this.enterEventLoop();
scope.enterEventLoop();
return requestors.length;
},
exitNestedEventLoop: function () {
requestors.pop();
this.leaveEventLoop();
scope.leaveEventLoop();
return requestors.length;
}
};

View File

@ -243,7 +243,6 @@ richlistitem:not([selected]) * {
.view-pane[type="experiment"] .addon:not([pending="uninstall"]) .pending,
.view-pane[type="experiment"] .disabled-postfix,
.view-pane[type="experiment"] .update-postfix,
.view-pane[type="experiment"] .version,
#detail-view[type="experiment"] .alert-container,
#detail-view[type="experiment"] #detail-version,
#detail-view[type="experiment"] #detail-creator {

View File

@ -460,6 +460,31 @@ var gEventManager = {
menuSep.hidden = (countMenuItemsBeforeSep == 0);
}, false);
let addonTooltip = document.getElementById("addonitem-tooltip");
addonTooltip.addEventListener("popupshowing", function() {
let addonItem = document.tooltipNode;
// The way the test triggers the tooltip the richlistitem is the
// tooltipNode but in normal use it is the anonymous node. This allows
// any case
if (addonItem.localName != "richlistitem")
addonItem = document.getBindingParent(addonItem);
let tiptext = addonItem.getAttribute("name");
if (addonItem.mAddon) {
if (shouldShowVersionNumber(addonItem.mAddon)) {
tiptext += " " + (addonItem.hasAttribute("upgrade") ? addonItem.mManualUpdate.version
: addonItem.mAddon.version);
}
}
else {
if (shouldShowVersionNumber(addonItem.mInstall))
tiptext += " " + addonItem.mInstall.version;
}
addonTooltip.label = tiptext;
}, false);
},
shutdown: function gEM_shutdown() {
@ -1448,6 +1473,10 @@ function shouldShowVersionNumber(aAddon) {
if (!aAddon.version)
return false;
// The version number is hidden for experiments.
if (aAddon.type == "experiment")
return false;
// The version number is hidden for lightweight themes.
if (aAddon.type == "theme")
return !/@personas\.mozilla\.org$/.test(aAddon.id);

View File

@ -795,8 +795,7 @@
<xul:hbox class="basicinfo-container">
<xul:hbox class="name-container">
<xul:label anonid="name" class="name" crop="end" flex="1"
xbl:inherits="value=name,tooltiptext=name"/>
<xul:label anonid="version" class="version"/>
tooltip="addonitem-tooltip" xbl:inherits="value=name"/>
<xul:label class="disabled-postfix" value="&addon.disabled.postfix;"/>
<xul:label class="update-postfix" value="&addon.update.postfix;"/>
<xul:spacer flex="5000"/> <!-- Necessary to make the name crop -->
@ -979,9 +978,6 @@
document.getAnonymousElementByAttribute(this, "anonid",
"info");
</field>
<field name="_version">
document.getAnonymousElementByAttribute(this, "anonid", "version");
</field>
<field name="_experimentState">
document.getAnonymousElementByAttribute(this, "anonid", "experiment-state");
</field>
@ -1118,11 +1114,6 @@
else
this._icon.src = "";
if (shouldShowVersionNumber(this.mAddon))
this._version.value = this.mAddon.version;
else
this._version.hidden = true;
if (this.mAddon.description)
this._description.value = this.mAddon.description;
else
@ -1414,14 +1405,6 @@
]]></body>
</method>
<method name="_updateUpgradeInfo">
<body><![CDATA[
// Only update the version string if we're displaying the upgrade info
if (this.hasAttribute("upgrade") && shouldShowVersionNumber(this.mAddon))
this._version.value = this.mManualUpdate.version;
]]></body>
</method>
<method name="_fetchReleaseNotes">
<parameter name="aURI"/>
<body><![CDATA[
@ -1711,7 +1694,6 @@
this.mManualUpdate = aInstall;
this._showStatus("update-available");
this._updateUpgradeInfo();
]]></body>
</method>
@ -1913,8 +1895,7 @@
</xul:vbox>
<xul:vbox class="fade name-outer-container" flex="1">
<xul:hbox class="name-container">
<xul:label anonid="name" class="name" crop="end"/>
<xul:label anonid="version" class="version" hidden="true"/>
<xul:label anonid="name" class="name" crop="end" tooltip="addonitem-tooltip"/>
</xul:hbox>
</xul:vbox>
<xul:vbox class="install-status-container">
@ -1936,9 +1917,6 @@
<field name="_name">
document.getAnonymousElementByAttribute(this, "anonid", "name");
</field>
<field name="_version">
document.getAnonymousElementByAttribute(this, "anonid", "version");
</field>
<field name="_warning">
document.getAnonymousElementByAttribute(this, "anonid", "warning");
</field>
@ -1966,14 +1944,6 @@
this._icon.src = this.mAddon.iconURL ||
(this.mInstall ? this.mInstall.iconURL : "");
this._name.value = this.mAddon.name;
if (this.mAddon.version) {
this._version.value = this.mAddon.version;
this._version.hidden = false;
} else {
this._version.hidden = true;
}
} else {
this._icon.src = this.mInstall.iconURL;
// AddonInstall.name isn't always available - fallback to filename
@ -1987,13 +1957,6 @@
url.QueryInterface(Components.interfaces.nsIURL);
this._name.value = url.fileName;
}
if (this.mInstall.version) {
this._version.value = this.mInstall.version;
this._version.hidden = false;
} else {
this._version.hidden = true;
}
}
if (this.mInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {

View File

@ -71,6 +71,8 @@
label="&cmd.about.label;"
accesskey="&cmd.about.accesskey;"/>
</menupopup>
<tooltip id="addonitem-tooltip"/>
</popupset>
<!-- global commands - these act on all addons, or affect the addons manager

View File

@ -9,9 +9,7 @@ var gManagerWindow;
var gCategoryUtilities;
var gProvider;
function test() {
waitForExplicitFinish();
add_task(function test() {
gProvider = new MockProvider();
gProvider.createAddons([{
@ -31,16 +29,9 @@ function test() {
version: "789"
}]);
open_manager(null, function(aWindow) {
gManagerWindow = aWindow;
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
run_next_test();
});
}
function end_test() {
close_manager(gManagerWindow, finish);
}
gManagerWindow = yield open_manager();
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
});
function get(aId) {
return gManagerWindow.document.getElementById(aId);
@ -54,58 +45,54 @@ function open_details(aList, aItem, aCallback) {
aList.ensureElementIsVisible(aItem);
EventUtils.synthesizeMouseAtCenter(aItem, { clickCount: 1 }, gManagerWindow);
EventUtils.synthesizeMouseAtCenter(aItem, { clickCount: 2 }, gManagerWindow);
wait_for_view_load(gManagerWindow, aCallback);
return new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
}
function check_addon_has_version(aList, aName, aVersion) {
let check_addon_has_version = Task.async(function*(aList, aName, aVersion) {
for (let i = 0; i < aList.itemCount; i++) {
let item = aList.getItemAtIndex(i);
if (get_node(item, "name").value === aName) {
ok(true, "Item with correct name found");
is(get_node(item, "version").value, aVersion, "Item has correct version");
let { version } = yield get_tooltip_info(item);
is(version, aVersion, "Item has correct version");
return item;
}
}
ok(false, "Item with correct name was not found");
return null;
}
add_test(function() {
gCategoryUtilities.openType("extension", function() {
info("Extension");
let list = gManagerWindow.document.getElementById("addon-list");
let item = check_addon_has_version(list, "Extension 1", "123");
open_details(list, item, function() {
is_element_visible(get("detail-version"), "Details view has version visible");
is(get("detail-version").value, "123", "Details view has correct version");
run_next_test();
});
});
});
add_test(function() {
gCategoryUtilities.openType("theme", function() {
info("Normal theme");
let list = gManagerWindow.document.getElementById("addon-list");
let item = check_addon_has_version(list, "Theme 2", "456");
open_details(list, item, function() {
is_element_visible(get("detail-version"), "Details view has version visible");
is(get("detail-version").value, "456", "Details view has correct version");
run_next_test();
});
});
add_task(function*() {
yield gCategoryUtilities.openType("extension");
info("Extension");
let list = gManagerWindow.document.getElementById("addon-list");
let item = yield check_addon_has_version(list, "Extension 1", "123");
yield open_details(list, item);
is_element_visible(get("detail-version"), "Details view has version visible");
is(get("detail-version").value, "123", "Details view has correct version");
});
add_test(function() {
gCategoryUtilities.openType("theme", function() {
info("Lightweight theme");
let list = gManagerWindow.document.getElementById("addon-list");
// See that the version isn't displayed
let item = check_addon_has_version(list, "Persona 3", "");
open_details(list, item, function() {
is_element_hidden(get("detail-version"), "Details view has version hidden");
// If the version element is hidden then we don't care about its value
run_next_test();
});
});
add_task(function*() {
yield gCategoryUtilities.openType("theme");
info("Normal theme");
let list = gManagerWindow.document.getElementById("addon-list");
let item = yield check_addon_has_version(list, "Theme 2", "456");
yield open_details(list, item);
is_element_visible(get("detail-version"), "Details view has version visible");
is(get("detail-version").value, "456", "Details view has correct version");
});
add_task(function*() {
yield gCategoryUtilities.openType("theme");
info("Lightweight theme");
let list = gManagerWindow.document.getElementById("addon-list");
// See that the version isn't displayed
let item = yield check_addon_has_version(list, "Persona 3", undefined);
yield open_details(list, item);
is_element_hidden(get("detail-version"), "Details view has version hidden");
// If the version element is hidden then we don't care about its value
});
add_task(function end_test() {
close_manager(gManagerWindow, finish);
});

View File

@ -8,19 +8,12 @@
var gManagerWindow;
var gCategoryUtilities;
function test() {
add_task(function* test() {
waitForExplicitFinish();
open_manager("addons://list/extension", function(aWindow) {
gManagerWindow = aWindow;
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
run_next_test();
});
}
function end_test() {
close_manager(gManagerWindow, finish);
}
gManagerWindow = yield open_manager("addons://list/extension");
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
});
function get_list_item_count() {
return get_test_items_in_list(gManagerWindow).length;
@ -34,21 +27,23 @@ function get_class_node(parent, cls) {
return parent.ownerDocument.getAnonymousElementByAttribute(parent, "class", cls);
}
function install_addon(aXpi, aCallback) {
AddonManager.getInstallForURL(TESTROOT + "addons/" + aXpi + ".xpi",
function(aInstall) {
aInstall.addListener({
onInstallEnded: function(aInstall) {
executeSoon(aCallback);
}
});
aInstall.install();
}, "application/x-xpinstall");
function install_addon(aXpi) {
return new Promise(resolve => {
AddonManager.getInstallForURL(TESTROOT + "addons/" + aXpi + ".xpi",
function(aInstall) {
aInstall.addListener({
onInstallEnded: function(aInstall) {
resolve();
}
});
aInstall.install();
}, "application/x-xpinstall");
});
}
function check_addon(aAddon, version) {
let check_addon = Task.async(function*(aAddon, aVersion) {
is(get_list_item_count(), 1, "Should be one item in the list");
is(aAddon.version, version, "Add-on should have the right version");
is(aAddon.version, aVersion, "Add-on should have the right version");
let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
ok(!!item, "Should see the add-on in the list");
@ -56,127 +51,108 @@ function check_addon(aAddon, version) {
// Force XBL to apply
item.clientTop;
is(get_node(item, "version").value, version, "Version should be correct");
let { version } = yield get_tooltip_info(item);
is(version, aVersion, "Version should be correct");
if (aAddon.userDisabled)
is_element_visible(get_class_node(item, "disabled-postfix"), "Disabled postfix should be hidden");
else
is_element_hidden(get_class_node(item, "disabled-postfix"), "Disabled postfix should be hidden");
}
});
// Install version 1 then upgrade to version 2 with the manager open
add_test(function() {
install_addon("browser_bug596336_1", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
check_addon(aAddon, "1.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
add_task(function() {
yield install_addon("browser_bug596336_1");
let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
yield check_addon(aAddon, "1.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
install_addon("browser_bug596336_2", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
check_addon(aAddon, "2.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
yield install_addon("browser_bug596336_2");
[aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
yield check_addon(aAddon, "2.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
aAddon.uninstall();
aAddon.uninstall();
is(get_list_item_count(), 0, "Should be no items in the list");
run_next_test();
});
});
});
});
is(get_list_item_count(), 0, "Should be no items in the list");
});
// Install version 1 mark it as disabled then upgrade to version 2 with the
// manager open
add_test(function() {
install_addon("browser_bug596336_1", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
aAddon.userDisabled = true;
check_addon(aAddon, "1.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
add_task(function() {
yield install_addon("browser_bug596336_1");
let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
aAddon.userDisabled = true;
yield check_addon(aAddon, "1.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
install_addon("browser_bug596336_2", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
check_addon(aAddon, "2.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
yield install_addon("browser_bug596336_2");
[aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
yield check_addon(aAddon, "2.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
aAddon.uninstall();
aAddon.uninstall();
is(get_list_item_count(), 0, "Should be no items in the list");
run_next_test();
});
});
});
});
is(get_list_item_count(), 0, "Should be no items in the list");
});
// Install version 1 click the remove button and then upgrade to version 2 with
// the manager open
add_test(function() {
install_addon("browser_bug596336_1", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
check_addon(aAddon, "1.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
add_task(function() {
yield install_addon("browser_bug596336_1");
let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
yield check_addon(aAddon, "1.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
// Force XBL to apply
item.clientTop;
// Force XBL to apply
item.clientTop;
ok(aAddon.userDisabled, "Add-on should be disabled");
ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
ok(aAddon.userDisabled, "Add-on should be disabled");
ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
install_addon("browser_bug596336_2", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
check_addon(aAddon, "2.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
yield install_addon("browser_bug596336_2");
[aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
yield check_addon(aAddon, "2.0");
ok(!aAddon.userDisabled, "Add-on should not be disabled");
aAddon.uninstall();
aAddon.uninstall();
is(get_list_item_count(), 0, "Should be no items in the list");
run_next_test();
});
});
});
});
is(get_list_item_count(), 0, "Should be no items in the list");
});
// Install version 1, disable it, click the remove button and then upgrade to
// version 2 with the manager open
add_test(function() {
install_addon("browser_bug596336_1", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
aAddon.userDisabled = true;
check_addon(aAddon, "1.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
add_task(function() {
yield install_addon("browser_bug596336_1");
let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
aAddon.userDisabled = true;
yield check_addon(aAddon, "1.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
// Force XBL to apply
item.clientTop;
// Force XBL to apply
item.clientTop;
ok(aAddon.userDisabled, "Add-on should be disabled");
ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
ok(aAddon.userDisabled, "Add-on should be disabled");
ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
install_addon("browser_bug596336_2", function() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
check_addon(aAddon, "2.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
yield install_addon("browser_bug596336_2");
[aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
yield check_addon(aAddon, "2.0");
ok(aAddon.userDisabled, "Add-on should be disabled");
aAddon.uninstall();
aAddon.uninstall();
is(get_list_item_count(), 0, "Should be no items in the list");
run_next_test();
});
});
});
});
is(get_list_item_count(), 0, "Should be no items in the list");
});
add_task(function end_test() {
close_manager(gManagerWindow, finish);
});

View File

@ -426,8 +426,8 @@ add_task(function testActivateRealExperiments() {
is_element_hidden(el, "warning-container should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "pending-container");
is_element_hidden(el, "pending-container should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "version");
is_element_hidden(el, "version should be hidden.");
let { version } = yield get_tooltip_info(item);
Assert.equal(version, undefined, "version should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
is_element_hidden(el, "disabled-postfix should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "update-postfix");
@ -459,8 +459,8 @@ add_task(function testActivateRealExperiments() {
is_element_hidden(el, "warning-container should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "pending-container");
is_element_hidden(el, "pending-container should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "version");
is_element_hidden(el, "version should be hidden.");
({ version }) = yield get_tooltip_info(item);
Assert.equal(version, undefined, "version should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
is_element_hidden(el, "disabled-postfix should be hidden.");
el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "update-postfix");

File diff suppressed because it is too large Load Diff

View File

@ -55,11 +55,17 @@ add_test(function() {
add_test(function() {
let finished = 0;
function maybeRunNext() {
if (++finished == 2)
run_next_test();
}
gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
gAvailableCategory.removeEventListener("CategoryBadgeUpdated", arguments.callee, false);
is(gCategoryUtilities.isVisible(gAvailableCategory), true, "Available Updates category should now be visible");
is(gAvailableCategory.badgeCount, 1, "Badge for Available Updates should now be 1");
run_next_test();
maybeRunNext();
}, false);
gCategoryUtilities.openType("extension", function() {
@ -71,7 +77,10 @@ add_test(function() {
}]);
var item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
is(item._version.value, "1.0", "Should still show the old version in the normal list");
get_tooltip_info(item).then(({ version }) => {
is(version, "1.0", "Should still show the old version in the tooltip");
maybeRunNext();
});
});
});
@ -92,66 +101,61 @@ add_test(function() {
var item = list.firstChild;
is(item.mAddon.id, "addon2@tests.mozilla.org", "Update item should be for the manually updating addon");
// for manual update items, update-related properties are updated asynchronously,
// so we poll for one of the expected changes to know when its done
function waitForAsyncInit() {
if (item._version.value == "1.1") {
run_next_test();
return;
}
info("Update item not initialized yet, checking again in 100ms");
setTimeout(waitForAsyncInit, 100);
}
waitForAsyncInit();
// The item in the list will be checking for update information asynchronously
// so we have to wait for it to complete. Doing the same async request should
// make our callback be called later.
AddonManager.getAllInstalls(run_next_test);
});
add_test(function() {
var list = gManagerWindow.document.getElementById("updates-list");
var item = list.firstChild;
is(item._version.value, "1.1", "Update item should have version number of the update");
var postfix = gManagerWindow.document.getAnonymousElementByAttribute(item, "class", "update-postfix");
is_element_visible(postfix, "'Update' postfix should be visible");
is_element_visible(item._updateAvailable, "");
is_element_visible(item._relNotesToggle, "Release notes toggle should be visible");
is_element_hidden(item._warning, "Incompatible warning should be hidden");
is_element_hidden(item._error, "Blocklist error should be hidden");
get_tooltip_info(item).then(({ version }) => {
is(version, "1.1", "Update item should have version number of the update");
var postfix = gManagerWindow.document.getAnonymousElementByAttribute(item, "class", "update-postfix");
is_element_visible(postfix, "'Update' postfix should be visible");
is_element_visible(item._updateAvailable, "");
is_element_visible(item._relNotesToggle, "Release notes toggle should be visible");
is_element_hidden(item._warning, "Incompatible warning should be hidden");
is_element_hidden(item._error, "Blocklist error should be hidden");
info("Opening release notes");
item.addEventListener("RelNotesToggle", function() {
item.removeEventListener("RelNotesToggle", arguments.callee, false);
info("Release notes now open");
is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
is_element_visible(item._relNotesError, "Release notes error message should be visible");
is(item._relNotes.childElementCount, 0, "Release notes should be empty");
info("Closing release notes");
info("Opening release notes");
item.addEventListener("RelNotesToggle", function() {
item.removeEventListener("RelNotesToggle", arguments.callee, false);
info("Release notes now closed");
info("Setting Release notes URI to something that should load");
gProvider.installs[0].releaseNotesURI = Services.io.newURI(TESTROOT + "releaseNotes.xhtml", null, null)
info("Release notes now open");
info("Re-opening release notes");
is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
is_element_visible(item._relNotesError, "Release notes error message should be visible");
is(item._relNotes.childElementCount, 0, "Release notes should be empty");
info("Closing release notes");
item.addEventListener("RelNotesToggle", function() {
item.removeEventListener("RelNotesToggle", arguments.callee, false);
info("Release notes now open");
info("Release notes now closed");
info("Setting Release notes URI to something that should load");
gProvider.installs[0].releaseNotesURI = Services.io.newURI(TESTROOT + "releaseNotes.xhtml", null, null)
is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
is_element_hidden(item._relNotesError, "Release notes error message should be hidden");
isnot(item._relNotes.childElementCount, 0, "Release notes should have been inserted into container");
run_next_test();
info("Re-opening release notes");
item.addEventListener("RelNotesToggle", function() {
item.removeEventListener("RelNotesToggle", arguments.callee, false);
info("Release notes now open");
is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
is_element_hidden(item._relNotesError, "Release notes error message should be hidden");
isnot(item._relNotes.childElementCount, 0, "Release notes should have been inserted into container");
run_next_test();
}, false);
EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
}, false);
EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
}, false);
EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
}, false);
EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
});
});

View File

@ -49,16 +49,20 @@ add_test(function() {
gProvider.installs[0]._addonToInstall = newAddon;
var item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
is(item._version.value, "1.0", "Should still show the old version in the normal list");
var name = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "name");
is(name.value, "manually updating addon", "Should show the old name in the list");
var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
is_element_visible(update, "Update button should be visible");
get_tooltip_info(item).then(({ name, version }) => {
is(name, "manually updating addon", "Should show the old name in the tooltip");
is(version, "1.0", "Should still show the old version in the tooltip");
item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
is(item, null, "Should not show the new version in the list");
var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
is_element_visible(update, "Update button should be visible");
run_next_test();
item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
is(item, null, "Should not show the new version in the list");
run_next_test();
});
});
});

View File

@ -227,6 +227,45 @@ function run_next_test() {
executeSoon(() => log_exceptions(test));
}
let get_tooltip_info = Task.async(function*(addon) {
let managerWindow = addon.ownerDocument.defaultView;
// The popup code uses a triggering event's target to set the
// document.tooltipNode property.
let nameNode = addon.ownerDocument.getAnonymousElementByAttribute(addon, "anonid", "name");
let event = new managerWindow.CustomEvent("TriggerEvent");
nameNode.dispatchEvent(event);
let tooltip = managerWindow.document.getElementById("addonitem-tooltip");
let promise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
tooltip.openPopup(nameNode, "after_start", 0, 0, false, false, event);
yield promise;
let tiptext = tooltip.label;
promise = BrowserTestUtils.waitForEvent(tooltip, "popuphidden");
tooltip.hidePopup();
yield promise;
let expectedName = addon.getAttribute("name");
ok(tiptext.substring(0, expectedName.length), expectedName,
"Tooltip should always start with the expected name");
if (expectedName.length == tiptext.length) {
return {
name: tiptext,
version: undefined
};
}
else {
return {
name: tiptext.substring(0, expectedName.length),
version: tiptext.substring(expectedName.length + 1)
};
}
});
function get_addon_file_url(aFilename) {
try {
var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].
@ -489,6 +528,11 @@ function is_element_hidden(aElement, aMsg) {
ok(is_hidden(aElement), aMsg || (aElement + " should be hidden"));
}
function promiseAddonsByIDs(aIDs) {
return new Promise(resolve => {
AddonManager.getAddonsByIDs(aIDs, resolve);
});
}
/**
* Install an add-on and call a callback when complete.
*