Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2015-07-21 16:08:37 -07:00
commit 68e284c50a
98 changed files with 1167 additions and 2234 deletions

View File

@ -1758,6 +1758,9 @@ pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
pref("loop.debug.twoWayMediaTelemetry", false);
pref("loop.feedback.dateLastSeenSec", 0);
pref("loop.feedback.periodSec", 15770000); // 6 months.
pref("loop.feedback.formURL", "http://www.surveygizmo.com/s3/2227372/Firefox-Hello-Product-Survey");
#ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else

View File

@ -17,7 +17,7 @@ content/js/conversation.js
content/js/conversationViews.js
content/js/panel.js
content/js/roomViews.js
content/shared/js/feedbackViews.js
content/js/feedbackViews.js
content/shared/js/textChatView.js
content/shared/js/views.js
standalone/content/js/fxOSMarketplace.js

View File

@ -27,7 +27,6 @@
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
@ -37,9 +36,8 @@
<script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="loop/js/feedbackViews.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>

View File

@ -6,14 +6,12 @@ var loop = loop || {};
loop.conversation = (function(mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var CallControllerView = loop.conversationViews.CallControllerView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
var FeedbackView = loop.feedbackViews.FeedbackView;
var GenericFailureView = loop.conversationViews.GenericFailureView;
/**
@ -24,6 +22,7 @@ loop.conversation = (function(mozL10n) {
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin
],
@ -37,19 +36,51 @@ loop.conversation = (function(mozL10n) {
return this.getStoreState();
},
_renderFeedbackForm: function() {
this.setTitle(mozL10n.get("conversation_has_ended"));
return (React.createElement(FeedbackView, {
mozLoop: this.props.mozLoop,
onAfterFeedbackReceived: this.closeWindow}));
},
/**
* We only show the feedback for once every 6 months, otherwise close
* the window.
*/
handleCallTerminated: function() {
var delta = new Date() - new Date(this.state.feedbackTimestamp);
// Show timestamp if feedback period (6 months) passed.
// 0 is default value for pref. Always show feedback form on first use.
if (this.state.feedbackTimestamp === 0 ||
delta >= this.state.feedbackPeriod) {
this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
return;
}
this.closeWindow();
},
render: function() {
if (this.state.showFeedbackForm) {
return this._renderFeedbackForm();
}
switch(this.state.windowType) {
// CallControllerView is used for both.
case "incoming":
case "outgoing": {
return (React.createElement(CallControllerView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop}));
mozLoop: this.props.mozLoop,
onCallTerminated: this.handleCallTerminated}));
}
case "room": {
return (React.createElement(DesktopRoomConversationView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop,
onCallTerminated: this.handleCallTerminated,
roomStore: this.props.roomStore}));
}
case "failed": {
@ -102,15 +133,6 @@ loop.conversation = (function(mozL10n) {
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
var appVersionInfo = navigator.mozLoop.appVersionInfo;
var feedbackClient = new loop.FeedbackAPIClient(
navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
product: navigator.mozLoop.getLoopPref("feedback.product"),
platform: appVersionInfo.OS,
channel: appVersionInfo.channel,
version: appVersionInfo.version
});
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
@ -131,9 +153,6 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop,
activeRoomStore: activeRoomStore
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
@ -141,7 +160,6 @@ loop.conversation = (function(mozL10n) {
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore,
feedbackStore: feedbackStore,
textChatStore: textChatStore
});

View File

@ -6,14 +6,12 @@ var loop = loop || {};
loop.conversation = (function(mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var CallControllerView = loop.conversationViews.CallControllerView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
var FeedbackView = loop.feedbackViews.FeedbackView;
var GenericFailureView = loop.conversationViews.GenericFailureView;
/**
@ -24,6 +22,7 @@ loop.conversation = (function(mozL10n) {
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin
],
@ -37,19 +36,51 @@ loop.conversation = (function(mozL10n) {
return this.getStoreState();
},
_renderFeedbackForm: function() {
this.setTitle(mozL10n.get("conversation_has_ended"));
return (<FeedbackView
mozLoop={this.props.mozLoop}
onAfterFeedbackReceived={this.closeWindow} />);
},
/**
* We only show the feedback for once every 6 months, otherwise close
* the window.
*/
handleCallTerminated: function() {
var delta = new Date() - new Date(this.state.feedbackTimestamp);
// Show timestamp if feedback period (6 months) passed.
// 0 is default value for pref. Always show feedback form on first use.
if (this.state.feedbackTimestamp === 0 ||
delta >= this.state.feedbackPeriod) {
this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
return;
}
this.closeWindow();
},
render: function() {
if (this.state.showFeedbackForm) {
return this._renderFeedbackForm();
}
switch(this.state.windowType) {
// CallControllerView is used for both.
case "incoming":
case "outgoing": {
return (<CallControllerView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop} />);
mozLoop={this.props.mozLoop}
onCallTerminated={this.handleCallTerminated} />);
}
case "room": {
return (<DesktopRoomConversationView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
onCallTerminated={this.handleCallTerminated}
roomStore={this.props.roomStore} />);
}
case "failed": {
@ -102,15 +133,6 @@ loop.conversation = (function(mozL10n) {
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
var appVersionInfo = navigator.mozLoop.appVersionInfo;
var feedbackClient = new loop.FeedbackAPIClient(
navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
product: navigator.mozLoop.getLoopPref("feedback.product"),
platform: appVersionInfo.OS,
channel: appVersionInfo.channel,
version: appVersionInfo.version
});
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
@ -131,9 +153,6 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop,
activeRoomStore: activeRoomStore
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
@ -141,7 +160,6 @@ loop.conversation = (function(mozL10n) {
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore,
feedbackStore: feedbackStore,
textChatStore: textChatStore
});

View File

@ -27,14 +27,26 @@ loop.store.ConversationAppStore = (function() {
this._dispatcher = options.dispatcher;
this._mozLoop = options.mozLoop;
this._storeState = {};
this._storeState = this.getInitialStoreState();
this._dispatcher.register(this, [
"getWindowData"
"getWindowData",
"showFeedbackForm"
]);
};
ConversationAppStore.prototype = _.extend({
getInitialStoreState: function() {
return {
// How often to display the form. Convert seconds to ms.
feedbackPeriod: this._mozLoop.getLoopPref("feedback.periodSec") * 1000,
// Date when the feedback form was last presented. Convert to ms.
feedbackTimestamp: this._mozLoop
.getLoopPref("feedback.dateLastSeenSec") * 1000,
showFeedbackForm: false
};
},
/**
* Retrieves current store state.
*
@ -54,6 +66,20 @@ loop.store.ConversationAppStore = (function() {
this.trigger("change");
},
/**
* Sets store state which will result in the feedback form rendered.
* Saves a timestamp of when the feedback was last rendered.
*/
showFeedbackForm: function() {
var timestamp = Math.floor(new Date().getTime() / 1000);
this._mozLoop.setLoopPref("feedback.dateLastSeenSec", timestamp);
this.setStoreState({
showFeedbackForm: true
});
},
/**
* Handles the get window data action - obtains the window data,
* updates the store and notifies interested components.

View File

@ -15,7 +15,6 @@ loop.conversationViews = (function(mozL10n) {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
@ -701,7 +700,8 @@ loop.conversationViews = (function(mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired
mozLoop: React.PropTypes.object.isRequired,
onCallTerminated: React.PropTypes.func.isRequired
},
getInitialState: function() {
@ -720,19 +720,6 @@ loop.conversationViews = (function(mozL10n) {
this.state.callState !== CALL_STATES.GATHER;
},
/**
* Used to setup and render the feedback view.
*/
_renderFeedbackView: function() {
this.setTitle(mozL10n.get("conversation_has_ended"));
return (
React.createElement(sharedViews.FeedbackView, {
onAfterFeedbackReceived: this._closeWindow.bind(this)}
)
);
},
_renderViewFromCallType: function() {
// For outgoing calls we can display the pending conversation view
// for any state that render() doesn't manage.
@ -760,6 +747,14 @@ loop.conversationViews = (function(mozL10n) {
return null;
},
componentDidUpdate: function(prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.callState === CALL_STATES.ONGOING &&
this.state.callState === CALL_STATES.FINISHED) {
this.props.onCallTerminated();
}
},
render: function() {
// Set the default title to the contact name or the callerId, note
// that views may override this, e.g. the feedback view.
@ -792,7 +787,10 @@ loop.conversationViews = (function(mozL10n) {
}
case CALL_STATES.FINISHED: {
this.play("terminated");
return this._renderFeedbackView();
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.

View File

@ -15,7 +15,6 @@ loop.conversationViews = (function(mozL10n) {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
@ -701,7 +700,8 @@ loop.conversationViews = (function(mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired
mozLoop: React.PropTypes.object.isRequired,
onCallTerminated: React.PropTypes.func.isRequired
},
getInitialState: function() {
@ -720,19 +720,6 @@ loop.conversationViews = (function(mozL10n) {
this.state.callState !== CALL_STATES.GATHER;
},
/**
* Used to setup and render the feedback view.
*/
_renderFeedbackView: function() {
this.setTitle(mozL10n.get("conversation_has_ended"));
return (
<sharedViews.FeedbackView
onAfterFeedbackReceived={this._closeWindow.bind(this)}
/>
);
},
_renderViewFromCallType: function() {
// For outgoing calls we can display the pending conversation view
// for any state that render() doesn't manage.
@ -760,6 +747,14 @@ loop.conversationViews = (function(mozL10n) {
return null;
},
componentDidUpdate: function(prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.callState === CALL_STATES.ONGOING &&
this.state.callState === CALL_STATES.FINISHED) {
this.props.onCallTerminated();
}
},
render: function() {
// Set the default title to the contact name or the callerId, note
// that views may override this, e.g. the feedback view.
@ -792,7 +787,10 @@ loop.conversationViews = (function(mozL10n) {
}
case CALL_STATES.FINISHED: {
this.play("terminated");
return this._renderFeedbackView();
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.

View File

@ -0,0 +1,47 @@
var loop = loop || {};
loop.feedbackViews = (function(_, mozL10n) {
"use strict";
/**
* Feedback view is displayed once every 6 months (loop.feedback.periodSec)
* after a conversation has ended.
*/
var FeedbackView = React.createClass({displayName: "FeedbackView",
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
/**
* Pressing the button to leave feedback will open the form in a new page
* and close the conversation window.
*/
onFeedbackButtonClick: function() {
var url = this.props.mozLoop.getLoopPref("feedback.formURL");
this.props.mozLoop.openURL(url);
this.props.onAfterFeedbackReceived();
},
render: function() {
return (
React.createElement("div", {className: "feedback-view-container"},
React.createElement("h2", {className: "feedback-heading"},
mozL10n.get("feedback_window_heading")
),
React.createElement("div", {className: "feedback-hello-logo"}),
React.createElement("div", {className: "feedback-button-container"},
React.createElement("button", {onClick: this.onFeedbackButtonClick,
ref: "feedbackFormBtn"},
mozL10n.get("feedback_request_button")
)
)
)
);
}
});
return {
FeedbackView: FeedbackView
};
})(_, navigator.mozL10n || document.mozL10n);

View File

@ -0,0 +1,47 @@
var loop = loop || {};
loop.feedbackViews = (function(_, mozL10n) {
"use strict";
/**
* Feedback view is displayed once every 6 months (loop.feedback.periodSec)
* after a conversation has ended.
*/
var FeedbackView = React.createClass({
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
/**
* Pressing the button to leave feedback will open the form in a new page
* and close the conversation window.
*/
onFeedbackButtonClick: function() {
var url = this.props.mozLoop.getLoopPref("feedback.formURL");
this.props.mozLoop.openURL(url);
this.props.onAfterFeedbackReceived();
},
render: function() {
return (
<div className="feedback-view-container">
<h2 className="feedback-heading">
{mozL10n.get("feedback_window_heading")}
</h2>
<div className="feedback-hello-logo" />
<div className="feedback-button-container">
<button onClick={this.onFeedbackButtonClick}
ref="feedbackFormBtn">
{mozL10n.get("feedback_request_button")}
</button>
</div>
</div>
);
}
});
return {
FeedbackView: FeedbackView
};
})(_, navigator.mozL10n || document.mozL10n);

View File

@ -558,6 +558,7 @@ loop.roomViews = (function(mozL10n) {
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
mozLoop: React.PropTypes.object.isRequired,
onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
@ -693,6 +694,14 @@ loop.roomViews = (function(mozL10n) {
this.setState({ showEditContext: false });
},
componentDidUpdate: function(prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.roomState === ROOM_STATES.ENDED &&
this.state.roomState === ROOM_STATES.ENDED) {
this.props.onCallTerminated();
}
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -726,10 +735,9 @@ loop.roomViews = (function(mozL10n) {
);
}
case ROOM_STATES.ENDED: {
return (
React.createElement(sharedViews.FeedbackView, {
onAfterFeedbackReceived: this.closeWindow})
);
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
default: {

View File

@ -558,6 +558,7 @@ loop.roomViews = (function(mozL10n) {
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
mozLoop: React.PropTypes.object.isRequired,
onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
@ -693,6 +694,14 @@ loop.roomViews = (function(mozL10n) {
this.setState({ showEditContext: false });
},
componentDidUpdate: function(prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.roomState === ROOM_STATES.ENDED &&
this.state.roomState === ROOM_STATES.ENDED) {
this.props.onCallTerminated();
}
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -726,10 +735,9 @@ loop.roomViews = (function(mozL10n) {
);
}
case ROOM_STATES.ENDED: {
return (
<sharedViews.FeedbackView
onAfterFeedbackReceived={this.closeWindow} />
);
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
default: {

View File

@ -536,7 +536,7 @@ html[dir="rtl"] .context-content {
width: 16px;
max-height: 16px;
margin-right: .8em;
flex: 0 1 auto;
flex: 0 0 auto;
}
html[dir="rtl"] .context-wrapper > .context-preview {

View File

@ -423,56 +423,47 @@
}
/* Feedback form */
.feedback {
padding: 14px;
}
.feedback p {
margin: 0px;
}
.feedback h3 {
color: #666;
font-size: 12px;
font-weight: 700;
text-align: center;
margin: 0 0 1em 0;
}
.feedback .faces {
.feedback-view-container {
display: flex;
flex-direction: row;
align-items: center;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
padding: 20px 0;
align-content: center;
align-items: flex-start;
height: 100%;
}
.feedback .face {
border: 1px solid transparent;
box-shadow: 0 1px 2px #CCC;
cursor: pointer;
border-radius: 4px;
margin: 0 10px;
width: 80px;
height: 80px;
background-color: #fbfbfb;
background-size: 60px auto;
background-position: center center;
.feedback-heading {
margin: 1em 0;
width: 100%;
text-align: center;
font-weight: bold;
font-size: 1.2em;
}
.feedback-hello-logo {
background-image: url("../img/helloicon.svg");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
flex: 2 1 auto;
width: 100%;
margin: 30px 0;
}
.feedback .face:hover {
border: 1px solid #DDD;
background-color: #FEFEFE;
.feedback-button-container {
flex: 0 1 auto;
margin: 30px;
align-self: center;
}
.feedback .face.face-happy {
background-image: url("../img/happy.png");
}
.feedback .face.face-sad {
background-image: url("../img/sad.png");
.feedback-button-container button {
margin: 0 30px;
padding: .5em 2em;
border: none;
background: #4E92DF;
color: #fff;
cursor: pointer;
}
.fx-embedded-btn-back {
@ -1547,7 +1538,6 @@ html[dir="rtl"] .text-chat-entry.received > p {
color: #aaa;
font-style: italic;
font-size: .8em;
order: 0;
flex: 0 1 auto;
align-self: center;
}
@ -1561,10 +1551,6 @@ html[dir="rtl"] .text-chat-entry.received > p {
order: 2;
}
.sent > .text-chat-entry-timestamp {
order: 0;
}
/* Pseudo element used to cover part between chat bubble and chat arrow. */
.text-chat-entry > p:after {
position: absolute;
@ -1631,7 +1617,6 @@ html[dir="rtl"] .text-chat-entry.received > p:after {
margin-right: -9px;
height: 10px;
background-image: url("../img/chatbubble-arrow-left.svg");
order: 0;
align-self: auto;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="evenodd" clip-rule="evenodd" fill="#4E92DF" d="M32 0C14.3 0 0 12.6 0 28.1c0 7.7 3.6 14.7 9.3 19.8-1 3.5-3 8.3-6.9 12.9.7 1.2 11.7-3 19.4-6.1 3.2.9 6.6 1.5 10.2 1.5 17.7 0 32-12.6 32-28.1S49.7 0 32 0zm9.6 16.9c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zm-19.3 0c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zM32 47.7h-.1-.1c-8.6 0-18.1-5.5-20.3-14.9 5.8 2.7 13.8 3.8 20.4 3.8 6.6 0 14.7-1.2 20.4-3.8-2.2 9.3-11.7 14.9-20.3 14.9z"/></svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -520,19 +520,18 @@ loop.shared.actions = (function() {
expires: Number
}),
/**
* Used to indicate that the feedback cycle is completed and the countdown
* finished.
*/
FeedbackComplete: Action.define("feedbackComplete", {
}),
/**
* Used to indicate the user wishes to leave the room.
*/
LeaveRoom: Action.define("leaveRoom", {
}),
/**
* Signals that the feedback view should be rendered.
*/
ShowFeedbackForm: Action.define("showFeedbackForm", {
}),
/**
* Used to record a link click for metrics purposes.
*/
@ -544,28 +543,6 @@ loop.shared.actions = (function() {
linkInfo: String
}),
/**
* Requires detailed information on sad feedback.
*/
RequireFeedbackDetails: Action.define("requireFeedbackDetails", {
}),
/**
* Send feedback data.
*/
SendFeedback: Action.define("sendFeedback", {
happy: Boolean,
category: String,
description: String
}),
/**
* Reacts on feedback submission error.
*/
SendFeedbackError: Action.define("sendFeedbackError", {
error: Error
}),
/**
* Used to inform of the current session, publisher and connection
* status.

View File

@ -1,115 +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/. */
var loop = loop || {};
loop.FeedbackAPIClient = (function($, _) {
"use strict";
/**
* Feedback API client. Sends feedback data to an input.mozilla.com compatible
* API.
*
* @param {String} baseUrl Base API url (required)
* @param {Object} defaults Defaults field values for that client.
*
* Required defaults:
* - {String} product Product name (required)
*
* Optional defaults:
* - {String} platform Platform name, eg. "Windows 8", "Android", "Linux"
* - {String} version Product version, eg. "22b2", "1.1"
* - {String} channel Product channel, eg. "stable", "beta"
* - {String} user_agent eg. Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0
*
* @link http://fjord.readthedocs.org/en/latest/api.html
*/
function FeedbackAPIClient(baseUrl, defaults) {
this.baseUrl = baseUrl;
if (!this.baseUrl) {
throw new Error("Missing required 'baseUrl' argument.");
}
this.defaults = defaults || {};
// required defaults checks
if (!this.defaults.hasOwnProperty("product")) {
throw new Error("Missing required 'product' default.");
}
}
FeedbackAPIClient.prototype = {
/**
* Supported field names by the feedback API.
* @type {Array}
*/
_supportedFields: ["happy",
"category",
"description",
"product",
"platform",
"version",
"channel",
"user_agent",
"url"],
/**
* Creates a formatted payload object compliant with the Feedback API spec
* against validated field data.
*
* @param {Object} fields Feedback initial values.
* @return {Object} Formatted payload object.
* @throws {Error} If provided values are invalid
*/
_createPayload: function(fields) {
if (typeof fields !== "object") {
throw new Error("Invalid feedback data provided.");
}
Object.keys(fields).forEach(function(name) {
if (this._supportedFields.indexOf(name) === -1) {
throw new Error("Unsupported field " + name);
}
}, this);
// Payload is basically defaults + fields merged in
var payload = _.extend({}, this.defaults, fields);
// Default description field value
if (!fields.description) {
payload.description = (fields.happy ? "Happy" : "Sad") + " User";
}
return payload;
},
/**
* Sends feedback data.
*
* @param {Object} fields Feedback form data.
* @param {Function} cb Callback(err, result)
*/
send: function(fields, cb) {
var req = $.ajax({
url: this.baseUrl,
method: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(this._createPayload(fields))
});
req.done(function(result) {
console.info("User feedback data have been submitted", result);
cb(null, result);
});
req.fail(function(jqXHR, textStatus, errorThrown) {
var message = "Error posting user feedback data";
var httpError = jqXHR.status + " " + errorThrown;
cb(new Error(message + ": " + httpError + "; " +
(jqXHR.responseJSON && jqXHR.responseJSON.detail || "")));
});
}
};
return FeedbackAPIClient;
})(jQuery, _);

View File

@ -1,105 +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/. */
var loop = loop || {};
loop.store = loop.store || {};
loop.store.FeedbackStore = (function() {
"use strict";
var sharedActions = loop.shared.actions;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES = {
// Initial state (mood selection)
INIT: "feedback-init",
// User detailed feedback form step
DETAILS: "feedback-details",
// Pending feedback data submission
PENDING: "feedback-pending",
// Feedback has been sent
SENT: "feedback-sent",
// There was an issue with the feedback API
FAILED: "feedback-failed"
};
/**
* Feedback store.
*
* @param {loop.Dispatcher} dispatcher The dispatcher for dispatching actions
* and registering to consume actions.
* @param {Object} options Options object:
* - {mozLoop} mozLoop The MozLoop API object.
* - {feedbackClient} loop.FeedbackAPIClient The feedback API client.
*/
var FeedbackStore = loop.store.createStore({
actions: [
"requireFeedbackDetails",
"sendFeedback",
"sendFeedbackError",
"feedbackComplete"
],
initialize: function(options) {
if (!options.feedbackClient) {
throw new Error("Missing option feedbackClient");
}
this._feedbackClient = options.feedbackClient;
},
/**
* Returns initial state data for this active room.
*/
getInitialStoreState: function() {
return {feedbackState: FEEDBACK_STATES.INIT};
},
/**
* Requires user detailed feedback.
*/
requireFeedbackDetails: function() {
this.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
},
/**
* Sends feedback data to the feedback server.
*
* @param {sharedActions.SendFeedback} actionData The action data.
*/
sendFeedback: function(actionData) {
delete actionData.name;
this._feedbackClient.send(actionData, function(err) {
if (err) {
this.dispatchAction(new sharedActions.SendFeedbackError({
error: err
}));
return;
}
this.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
}.bind(this));
this.setStoreState({feedbackState: FEEDBACK_STATES.PENDING});
},
/**
* Notifies a store from any error encountered while sending feedback data.
*
* @param {sharedActions.SendFeedback} actionData The action data.
*/
sendFeedbackError: function(actionData) {
this.setStoreState({
feedbackState: FEEDBACK_STATES.FAILED,
error: actionData.error
});
},
/**
* Resets the store to its initial state as feedback has been completed,
* i.e. ready for the next round of feedback.
*/
feedbackComplete: function() {
this.resetStoreState();
}
});
return FeedbackStore;
})();

View File

@ -1,323 +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/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.FeedbackView = (function(l10n) {
"use strict";
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
/**
* Feedback outer layout.
*
* Props:
* -
*/
var FeedbackLayout = React.createClass({displayName: "FeedbackLayout",
propTypes: {
children: React.PropTypes.element,
reset: React.PropTypes.func, // if not specified, no Back btn is shown
title: React.PropTypes.string.isRequired
},
render: function() {
var backButton = React.createElement("div", null);
if (this.props.reset) {
backButton = (
React.createElement("button", {className: "fx-embedded-btn-back",
onClick: this.props.reset,
type: "button"},
"« ", l10n.get("feedback_back_button")
)
);
}
return (
React.createElement("div", {className: "feedback"},
backButton,
React.createElement("h3", null, this.props.title),
this.props.children
)
);
}
});
/**
* Detailed feedback form.
*/
var FeedbackForm = React.createClass({displayName: "FeedbackForm",
propTypes: {
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
pending: React.PropTypes.bool,
reset: React.PropTypes.func
},
getInitialState: function() {
return {category: "", description: ""};
},
getDefaultProps: function() {
return {pending: false};
},
_getCategories: function() {
return {
audio_quality: l10n.get("feedback_category_audio_quality"),
video_quality: l10n.get("feedback_category_video_quality"),
disconnected: l10n.get("feedback_category_was_disconnected"),
confusing: l10n.get("feedback_category_confusing2"),
other: l10n.get("feedback_category_other2")
};
},
_getCategoryFields: function() {
var categories = this._getCategories();
return Object.keys(categories).map(function(category, key) {
return (
React.createElement("label", {className: "feedback-category-label", key: key},
React.createElement("input", {
checked: this.state.category === category,
className: "feedback-category-radio",
name: "category",
onChange: this.handleCategoryChange,
ref: "category",
type: "radio",
value: category}),
categories[category]
)
);
}, this);
},
/**
* Checks if the form is ready for submission:
*
* - no feedback submission should be pending.
* - a category (reason) must be chosen;
* - if the "other" category is chosen, a custom description must have been
* entered by the end user;
*
* @return {Boolean}
*/
_isFormReady: function() {
if (this.props.pending || !this.state.category) {
return false;
}
if (this.state.category === "other" && !this.state.description) {
return false;
}
return true;
},
handleCategoryChange: function(event) {
var category = event.target.value;
this.setState({
category: category
});
if (category == "other") {
this.refs.description.getDOMNode().focus();
}
},
handleDescriptionFieldChange: function(event) {
this.setState({description: event.target.value});
},
handleFormSubmit: function(event) {
event.preventDefault();
// XXX this feels ugly, we really want a feedbackActions object here.
this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
happy: false,
category: this.state.category,
description: this.state.description
}));
},
render: function() {
return (
React.createElement(FeedbackLayout, {
reset: this.props.reset,
title: l10n.get("feedback_category_list_heading")},
React.createElement("form", {onSubmit: this.handleFormSubmit},
this._getCategoryFields(),
React.createElement("p", null,
React.createElement("input", {className: "feedback-description",
name: "description",
onChange: this.handleDescriptionFieldChange,
placeholder:
l10n.get("feedback_custom_category_text_placeholder"),
ref: "description",
type: "text",
value: this.state.description})
),
React.createElement("button", {className: "btn btn-success",
disabled: !this._isFormReady(),
type: "submit"},
l10n.get("feedback_submit_button")
)
)
)
);
}
});
/**
* Feedback received view.
*
* Props:
* - {Function} onAfterFeedbackReceived Function to execute after the
* WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
*/
var FeedbackReceived = React.createClass({displayName: "FeedbackReceived",
propTypes: {
noCloseText: React.PropTypes.bool,
onAfterFeedbackReceived: React.PropTypes.func
},
getInitialState: function() {
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
},
componentDidMount: function() {
this._timer = setInterval(function() {
if (this.state.countdown == 1) {
clearInterval(this._timer);
if (this.props.onAfterFeedbackReceived) {
this.props.onAfterFeedbackReceived();
}
return;
}
this.setState({countdown: this.state.countdown - 1});
}.bind(this), 1000);
},
componentWillUnmount: function() {
if (this._timer) {
clearInterval(this._timer);
}
},
_renderCloseText: function() {
if (this.props.noCloseText) {
return null;
}
return (
React.createElement("p", {className: "info thank-you"},
l10n.get("feedback_window_will_close_in2", {
countdown: this.state.countdown,
num: this.state.countdown
}))
);
},
render: function() {
return (
React.createElement(FeedbackLayout, {title: l10n.get("feedback_thank_you_heading")},
this._renderCloseText()
)
);
}
});
/**
* Feedback view.
*/
var FeedbackView = React.createClass({displayName: "FeedbackView",
mixins: [
Backbone.Events,
loop.store.StoreMixin("feedbackStore")
],
propTypes: {
// Used by the UI showcase.
feedbackState: React.PropTypes.string,
noCloseText: React.PropTypes.bool,
onAfterFeedbackReceived: React.PropTypes.func
},
getInitialState: function() {
var storeState = this.getStoreState();
return _.extend({}, storeState, {
feedbackState: this.props.feedbackState || storeState.feedbackState
});
},
reset: function() {
this.setState(this.getStore().getInitialStoreState());
},
handleHappyClick: function() {
// XXX: If the user is happy, we directly send this information to the
// feedback API; this is a behavior we might want to revisit later.
this.getStore().dispatchAction(new sharedActions.SendFeedback({
happy: true,
category: "",
description: ""
}));
},
handleSadClick: function() {
this.getStore().dispatchAction(
new sharedActions.RequireFeedbackDetails());
},
_onFeedbackSent: function(err) {
if (err) {
// XXX better end user error reporting, see bug 1046738
console.error("Unable to send user feedback", err);
}
this.setState({pending: false, step: "finished"});
},
render: function() {
switch(this.state.feedbackState) {
default:
case FEEDBACK_STATES.INIT: {
return (
React.createElement(FeedbackLayout, {title:
l10n.get("feedback_call_experience_heading2")},
React.createElement("div", {className: "faces"},
React.createElement("button", {className: "face face-happy",
onClick: this.handleHappyClick}),
React.createElement("button", {className: "face face-sad",
onClick: this.handleSadClick})
)
)
);
}
case FEEDBACK_STATES.DETAILS: {
return (
React.createElement(FeedbackForm, {
feedbackStore: this.getStore(),
pending: this.state.feedbackState === FEEDBACK_STATES.PENDING,
reset: this.reset})
);
}
case FEEDBACK_STATES.PENDING:
case FEEDBACK_STATES.SENT:
case FEEDBACK_STATES.FAILED: {
if (this.state.error) {
// XXX better end user error reporting, see bug 1046738
console.error("Error encountered while submitting feedback",
this.state.error);
}
return (
React.createElement(FeedbackReceived, {
noCloseText: this.props.noCloseText,
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
);
}
}
}
});
return FeedbackView;
})(navigator.mozL10n || document.mozL10n);

View File

@ -1,323 +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/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.FeedbackView = (function(l10n) {
"use strict";
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
/**
* Feedback outer layout.
*
* Props:
* -
*/
var FeedbackLayout = React.createClass({
propTypes: {
children: React.PropTypes.element,
reset: React.PropTypes.func, // if not specified, no Back btn is shown
title: React.PropTypes.string.isRequired
},
render: function() {
var backButton = <div />;
if (this.props.reset) {
backButton = (
<button className="fx-embedded-btn-back"
onClick={this.props.reset}
type="button" >
&laquo;&nbsp;{l10n.get("feedback_back_button")}
</button>
);
}
return (
<div className="feedback">
{backButton}
<h3>{this.props.title}</h3>
{this.props.children}
</div>
);
}
});
/**
* Detailed feedback form.
*/
var FeedbackForm = React.createClass({
propTypes: {
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
pending: React.PropTypes.bool,
reset: React.PropTypes.func
},
getInitialState: function() {
return {category: "", description: ""};
},
getDefaultProps: function() {
return {pending: false};
},
_getCategories: function() {
return {
audio_quality: l10n.get("feedback_category_audio_quality"),
video_quality: l10n.get("feedback_category_video_quality"),
disconnected: l10n.get("feedback_category_was_disconnected"),
confusing: l10n.get("feedback_category_confusing2"),
other: l10n.get("feedback_category_other2")
};
},
_getCategoryFields: function() {
var categories = this._getCategories();
return Object.keys(categories).map(function(category, key) {
return (
<label className="feedback-category-label" key={key}>
<input
checked={this.state.category === category}
className="feedback-category-radio"
name="category"
onChange={this.handleCategoryChange}
ref="category"
type="radio"
value={category} />
{categories[category]}
</label>
);
}, this);
},
/**
* Checks if the form is ready for submission:
*
* - no feedback submission should be pending.
* - a category (reason) must be chosen;
* - if the "other" category is chosen, a custom description must have been
* entered by the end user;
*
* @return {Boolean}
*/
_isFormReady: function() {
if (this.props.pending || !this.state.category) {
return false;
}
if (this.state.category === "other" && !this.state.description) {
return false;
}
return true;
},
handleCategoryChange: function(event) {
var category = event.target.value;
this.setState({
category: category
});
if (category == "other") {
this.refs.description.getDOMNode().focus();
}
},
handleDescriptionFieldChange: function(event) {
this.setState({description: event.target.value});
},
handleFormSubmit: function(event) {
event.preventDefault();
// XXX this feels ugly, we really want a feedbackActions object here.
this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
happy: false,
category: this.state.category,
description: this.state.description
}));
},
render: function() {
return (
<FeedbackLayout
reset={this.props.reset}
title={l10n.get("feedback_category_list_heading")}>
<form onSubmit={this.handleFormSubmit}>
{this._getCategoryFields()}
<p>
<input className="feedback-description"
name="description"
onChange={this.handleDescriptionFieldChange}
placeholder={
l10n.get("feedback_custom_category_text_placeholder")}
ref="description"
type="text"
value={this.state.description} />
</p>
<button className="btn btn-success"
disabled={!this._isFormReady()}
type="submit">
{l10n.get("feedback_submit_button")}
</button>
</form>
</FeedbackLayout>
);
}
});
/**
* Feedback received view.
*
* Props:
* - {Function} onAfterFeedbackReceived Function to execute after the
* WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
*/
var FeedbackReceived = React.createClass({
propTypes: {
noCloseText: React.PropTypes.bool,
onAfterFeedbackReceived: React.PropTypes.func
},
getInitialState: function() {
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
},
componentDidMount: function() {
this._timer = setInterval(function() {
if (this.state.countdown == 1) {
clearInterval(this._timer);
if (this.props.onAfterFeedbackReceived) {
this.props.onAfterFeedbackReceived();
}
return;
}
this.setState({countdown: this.state.countdown - 1});
}.bind(this), 1000);
},
componentWillUnmount: function() {
if (this._timer) {
clearInterval(this._timer);
}
},
_renderCloseText: function() {
if (this.props.noCloseText) {
return null;
}
return (
<p className="info thank-you">{
l10n.get("feedback_window_will_close_in2", {
countdown: this.state.countdown,
num: this.state.countdown
})}</p>
);
},
render: function() {
return (
<FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
{this._renderCloseText()}
</FeedbackLayout>
);
}
});
/**
* Feedback view.
*/
var FeedbackView = React.createClass({
mixins: [
Backbone.Events,
loop.store.StoreMixin("feedbackStore")
],
propTypes: {
// Used by the UI showcase.
feedbackState: React.PropTypes.string,
noCloseText: React.PropTypes.bool,
onAfterFeedbackReceived: React.PropTypes.func
},
getInitialState: function() {
var storeState = this.getStoreState();
return _.extend({}, storeState, {
feedbackState: this.props.feedbackState || storeState.feedbackState
});
},
reset: function() {
this.setState(this.getStore().getInitialStoreState());
},
handleHappyClick: function() {
// XXX: If the user is happy, we directly send this information to the
// feedback API; this is a behavior we might want to revisit later.
this.getStore().dispatchAction(new sharedActions.SendFeedback({
happy: true,
category: "",
description: ""
}));
},
handleSadClick: function() {
this.getStore().dispatchAction(
new sharedActions.RequireFeedbackDetails());
},
_onFeedbackSent: function(err) {
if (err) {
// XXX better end user error reporting, see bug 1046738
console.error("Unable to send user feedback", err);
}
this.setState({pending: false, step: "finished"});
},
render: function() {
switch(this.state.feedbackState) {
default:
case FEEDBACK_STATES.INIT: {
return (
<FeedbackLayout title={
l10n.get("feedback_call_experience_heading2")}>
<div className="faces">
<button className="face face-happy"
onClick={this.handleHappyClick}></button>
<button className="face face-sad"
onClick={this.handleSadClick}></button>
</div>
</FeedbackLayout>
);
}
case FEEDBACK_STATES.DETAILS: {
return (
<FeedbackForm
feedbackStore={this.getStore()}
pending={this.state.feedbackState === FEEDBACK_STATES.PENDING}
reset={this.reset} />
);
}
case FEEDBACK_STATES.PENDING:
case FEEDBACK_STATES.SENT:
case FEEDBACK_STATES.FAILED: {
if (this.state.error) {
// XXX better end user error reporting, see bug 1046738
console.error("Error encountered while submitting feedback",
this.state.error);
}
return (
<FeedbackReceived
noCloseText={this.props.noCloseText}
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
);
}
}
}
});
return FeedbackView;
})(navigator.mozL10n || document.mozL10n);

View File

@ -22,7 +22,6 @@ loop.shared.mixins = (function() {
* @param {Object}
*/
function setRootObject(obj) {
// console.log("loop.shared.mixins: rootObject set to " + obj);
rootObject = obj;
}

View File

@ -20,6 +20,7 @@ browser.jar:
content/browser/loop/js/conversationViews.js (content/js/conversationViews.js)
content/browser/loop/js/roomStore.js (content/js/roomStore.js)
content/browser/loop/js/roomViews.js (content/js/roomViews.js)
content/browser/loop/js/feedbackViews.js (content/js/feedbackViews.js)
# Desktop styles
content/browser/loop/css/contacts.css (content/css/contacts.css)
@ -31,8 +32,7 @@ browser.jar:
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
# Shared images
content/browser/loop/shared/img/happy.png (content/shared/img/happy.png)
content/browser/loop/shared/img/sad.png (content/shared/img/sad.png)
content/browser/loop/shared/img/helloicon.svg (content/shared/img/helloicon.svg)
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
content/browser/loop/shared/img/spinner.svg (content/shared/img/spinner.svg)
@ -81,14 +81,11 @@ browser.jar:
content/browser/loop/shared/js/roomStates.js (content/shared/js/roomStates.js)
content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
content/browser/loop/shared/js/feedbackStore.js (content/shared/js/feedbackStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/feedbackViews.js (content/shared/js/feedbackViews.js)
content/browser/loop/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
content/browser/loop/shared/js/textChatView.js (content/shared/js/textChatView.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)

View File

@ -337,28 +337,6 @@ p.standalone-btn-label {
text-align: start;
}
.standalone .ended-conversation .feedback {
position: absolute;
width: 50%;
max-width: 400px;
margin: 10px auto;
top: 20px;
left: 10%;
right: 10%;
background: #FFF;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
border-radius: 3px;
z-index: 1002; /* ensures the form is always on top of the control bar */
}
.standalone .room-conversation-wrapper .ended-conversation .feedback {
right: 35%;
}
html[dir="rtl"] .standalone .room-conversation-wrapper .ended-conversation .feedback {
right: auto;
left: 35%;
}
.standalone .ended-conversation .local-stream {
/* Hide local media stream when feedback form is shown. */
display: none;

View File

@ -134,7 +134,6 @@
<script type="text/javascript" src="shared/js/crypto.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script>
@ -144,7 +143,6 @@
<script type="text/javascript" src="shared/js/roomStates.js"></script>
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="shared/js/textChatStore.js"></script>

View File

@ -90,13 +90,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
roomUsed: React.PropTypes.bool.isRequired
},
onFeedbackSent: function() {
// We pass a tick to prevent React warnings regarding nested updates.
setTimeout(function() {
this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
}.bind(this));
},
_renderCallToActionLink: function() {
if (this.props.isFirefox) {
return (
@ -118,8 +111,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
render: function() {
switch(this.props.roomState) {
case ROOM_STATES.ENDED:
case ROOM_STATES.READY: {
// XXX: In ENDED state, we should rather display the feedback form.
return (
React.createElement("div", {className: "room-inner-info-area"},
React.createElement("button", {className: "btn btn-join btn-info",
@ -172,22 +165,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
)
);
}
case ROOM_STATES.ENDED: {
if (this.props.roomUsed) {
return (
React.createElement("div", {className: "ended-conversation"},
React.createElement(sharedViews.FeedbackView, {
noCloseText: true,
onAfterFeedbackReceived: this.onFeedbackSent})
)
);
}
// In case the room was not used (no one was here), we
// bypass the feedback form.
this.onFeedbackSent();
return null;
}
case ROOM_STATES.FAILED: {
return (
React.createElement(StandaloneRoomFailureView, {

View File

@ -90,13 +90,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
roomUsed: React.PropTypes.bool.isRequired
},
onFeedbackSent: function() {
// We pass a tick to prevent React warnings regarding nested updates.
setTimeout(function() {
this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
}.bind(this));
},
_renderCallToActionLink: function() {
if (this.props.isFirefox) {
return (
@ -118,8 +111,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
render: function() {
switch(this.props.roomState) {
case ROOM_STATES.ENDED:
case ROOM_STATES.READY: {
// XXX: In ENDED state, we should rather display the feedback form.
return (
<div className="room-inner-info-area">
<button className="btn btn-join btn-info"
@ -172,22 +165,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
</div>
);
}
case ROOM_STATES.ENDED: {
if (this.props.roomUsed) {
return (
<div className="ended-conversation">
<sharedViews.FeedbackView
noCloseText={true}
onAfterFeedbackReceived={this.onFeedbackSent} />
</div>
);
}
// In case the room was not used (no one was here), we
// bypass the feedback form.
this.onFeedbackSent();
return null;
}
case ROOM_STATES.FAILED: {
return (
<StandaloneRoomFailureView

View File

@ -591,8 +591,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
currentStatus: mozL10n.get("status_conversation_ended")});
return (
React.createElement("div", {className: "ended-conversation"},
React.createElement(sharedViews.FeedbackView, {
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}),
React.createElement(sharedViews.ConversationView, {
audio: {enabled: false, visible: false},
dispatcher: this.props.dispatcher,
@ -684,7 +682,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
resetCallStatus: function() {
this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
return function() {
this.setState({callStatus: "start"});
}.bind(this);
@ -1024,13 +1021,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
var feedbackApiClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
user_agent: navigator.userAgent,
url: document.location.origin
});
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
@ -1067,22 +1057,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
sdkDriver: sdkDriver
});
var feedbackClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
user_agent: navigator.userAgent,
url: document.location.origin
});
// Stores
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
sdk: OT
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: activeRoomStore
});
@ -1092,7 +1072,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
feedbackStore: feedbackStore,
// This isn't used in any views, but is saved here to ensure it
// is kept alive.
standaloneMetricsStore: standaloneMetricsStore,

View File

@ -591,8 +591,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
currentStatus: mozL10n.get("status_conversation_ended")});
return (
<div className="ended-conversation">
<sharedViews.FeedbackView
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
<sharedViews.ConversationView
audio={{enabled: false, visible: false}}
dispatcher={this.props.dispatcher}
@ -684,7 +682,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
resetCallStatus: function() {
this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
return function() {
this.setState({callStatus: "start"});
}.bind(this);
@ -1024,13 +1021,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
var feedbackApiClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
user_agent: navigator.userAgent,
url: document.location.origin
});
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
@ -1067,22 +1057,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
sdkDriver: sdkDriver
});
var feedbackClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
user_agent: navigator.userAgent,
url: document.location.origin
});
// Stores
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
sdk: OT
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: activeRoomStore
});
@ -1092,7 +1072,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
feedbackStore: feedbackStore,
// This isn't used in any views, but is saved here to ensure it
// is kept alive.
standaloneMetricsStore: standaloneMetricsStore,

View File

@ -73,27 +73,6 @@ call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
feedback_call_experience_heading2=How was your conversation?
feedback_thank_you_heading=Thank you for your feedback!
feedback_category_list_heading=What made you sad?
feedback_category_audio_quality=Audio quality
feedback_category_video_quality=Video quality
feedback_category_was_disconnected=Was disconnected
feedback_category_confusing2=Confusing controls
feedback_category_other2=Other
feedback_custom_category_text_placeholder=What went wrong?
feedback_submit_button=Submit
feedback_back_button=Back
## LOCALIZATION NOTE (feedback_window_will_close_in2):
## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
## In this item, don't translate the part between {{..}}
feedback_window_will_close_in2={[ plural(countdown) ]}
feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback

View File

@ -32,7 +32,8 @@ describe("loop.store.ConversationAppStore", function () {
});
describe("#getWindowData", function() {
var fakeWindowData, fakeGetWindowData, fakeMozLoop, store;
var fakeWindowData, fakeGetWindowData, fakeMozLoop, store, getLoopPrefStub;
var setLoopPrefStub;
beforeEach(function() {
fakeWindowData = {
@ -44,13 +45,18 @@ describe("loop.store.ConversationAppStore", function () {
windowId: "42"
};
getLoopPrefStub = sandbox.stub();
setLoopPrefStub = sandbox.stub();
fakeMozLoop = {
getConversationWindowData: function(windowId) {
if (windowId === "42") {
return fakeWindowData;
}
return null;
}
},
getLoopPref: getLoopPrefStub,
setLoopPref: setLoopPrefStub
};
store = new loop.store.ConversationAppStore({
@ -59,6 +65,10 @@ describe("loop.store.ConversationAppStore", function () {
});
});
afterEach(function() {
sandbox.restore();
});
it("should fetch the window type from the mozLoop API", function() {
dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
@ -67,6 +77,52 @@ describe("loop.store.ConversationAppStore", function () {
});
});
it("should have the feedback period in initial state", function() {
getLoopPrefStub.returns(42);
// Expect ms.
expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
});
it("should have the dateLastSeen in initial state", function() {
getLoopPrefStub.returns(42);
// Expect ms.
expect(store.getInitialStoreState().feedbackTimestamp).to.eql(42 * 1000);
});
it("should fetch the correct pref for feedback period", function() {
store.getInitialStoreState();
sinon.assert.calledWithExactly(getLoopPrefStub, "feedback.periodSec");
});
it("should fetch the correct pref for feedback period", function() {
store.getInitialStoreState();
sinon.assert.calledWithExactly(getLoopPrefStub,
"feedback.dateLastSeenSec");
});
it("should set showFeedbackForm to true when action is triggered", function() {
var showFeedbackFormStub = sandbox.stub(store, "showFeedbackForm");
dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
sinon.assert.calledOnce(showFeedbackFormStub);
});
it("should set feedback timestamp on ShowFeedbackForm action", function() {
var clock = sandbox.useFakeTimers();
// Make sure we round down the value.
clock.tick(1001);
dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
sinon.assert.calledOnce(setLoopPrefStub);
sinon.assert.calledWithExactly(setLoopPrefStub,
"feedback.dateLastSeenSec", 1);
});
it("should dispatch a SetupWindowData action with the data from the mozLoop API",
function() {
sandbox.stub(dispatcher, "dispatch");
@ -80,5 +136,4 @@ describe("loop.store.ConversationAppStore", function () {
}, fakeWindowData)));
});
});
});

View File

@ -10,7 +10,7 @@ describe("loop.conversationViews", function () {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sandbox, view, dispatcher, contact, fakeAudioXHR, conversationStore;
var fakeMozLoop, fakeWindow;
var fakeMozLoop, fakeWindow, fakeClock;
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
@ -20,7 +20,7 @@ describe("loop.conversationViews", function () {
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
fakeClock = sandbox.useFakeTimers();
sandbox.stub(document.mozL10n, "get", function(x) {
return x;
@ -58,6 +58,7 @@ describe("loop.conversationViews", function () {
},
// Dummy function, stubbed below.
getLoopPref: function() {},
setLoopPref: sandbox.stub(),
calls: {
clearCallInProgress: sinon.stub()
},
@ -95,9 +96,6 @@ describe("loop.conversationViews", function () {
};
loop.shared.mixins.setRootObject(fakeWindow);
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: fakeMozLoop,
@ -105,8 +103,7 @@ describe("loop.conversationViews", function () {
});
loop.store.StoreMixin.register({
conversationStore: conversationStore,
feedbackStore: feedbackStore
conversationStore: conversationStore
});
});
@ -608,20 +605,23 @@ describe("loop.conversationViews", function () {
});
describe("CallControllerView", function() {
var feedbackStore;
var onCallTerminatedStub;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.CallControllerView, {
dispatcher: dispatcher,
mozLoop: fakeMozLoop
mozLoop: fakeMozLoop,
onCallTerminated: onCallTerminatedStub
}));
}
beforeEach(function() {
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
onCallTerminatedStub = sandbox.stub();
});
afterEach(function() {
sandbox.restore();
});
it("should set the document title to the callerId", function() {
@ -704,24 +704,6 @@ describe("loop.conversationViews", function () {
loop.conversationViews.OngoingConversationView);
});
it("should render the FeedbackView when the call state is 'finished'",
function() {
conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.shared.views.FeedbackView);
});
it("should set the document title to conversation_has_ended when displaying the feedback view", function() {
conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
mountTestComponent();
expect(fakeWindow.document.title).eql("conversation_has_ended");
});
it("should play the terminated sound when the call state is 'finished'",
function() {
var fakeAudio = {
@ -756,6 +738,21 @@ describe("loop.conversationViews", function () {
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.CallFailedView);
});
it("should call onCallTerminated when the call is finished", function() {
conversationStore.setStoreState({
callState: CALL_STATES.ONGOING
});
view = mountTestComponent({
callState: CALL_STATES.FINISHED
});
// Force a state change so that it triggers componentDidUpdate.
view.setState({ callState: CALL_STATES.FINISHED });
sinon.assert.calledOnce(onCallTerminatedStub);
sinon.assert.calledWithExactly(onCallTerminatedStub);
});
});
describe("AcceptCallView", function() {

View File

@ -6,13 +6,15 @@ describe("loop.conversation", function() {
"use strict";
var expect = chai.expect;
var FeedbackView = loop.feedbackViews.FeedbackView;
var TestUtils = React.addons.TestUtils;
var sharedModels = loop.shared.models,
fakeWindow,
sandbox;
var sharedActions = loop.shared.actions;
var sharedModels = loop.shared.models;
var fakeWindow, sandbox, getLoopPrefStub, setLoopPrefStub, mozL10nGet;
beforeEach(function() {
sandbox = sinon.sandbox.create();
setLoopPrefStub = sandbox.stub();
navigator.mozLoop = {
doNotDisturb: true,
@ -22,7 +24,7 @@ describe("loop.conversation", function() {
get locale() {
return "en-US";
},
setLoopPref: sinon.stub(),
setLoopPref: setLoopPrefStub,
getLoopPref: function(prefName) {
if (prefName == "debug.sdk") {
return false;
@ -63,7 +65,7 @@ describe("loop.conversation", function() {
// XXX These stubs should be hoisted in a common file
// Bug 1040968
sandbox.stub(document.mozL10n, "get", function(x) {
mozL10nGet = sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
document.mozL10n.initialize(navigator.mozLoop);
@ -132,7 +134,7 @@ describe("loop.conversation", function() {
describe("AppControllerView", function() {
var conversationStore, client, ccView, dispatcher;
var conversationAppStore, roomStore;
var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
@ -215,5 +217,97 @@ describe("loop.conversation", function() {
TestUtils.findRenderedComponentWithType(ccView,
loop.conversationViews.GenericFailureView);
});
it("should set the correct title when rendering feedback view", function() {
conversationAppStore.setStoreState({showFeedbackForm: true});
ccView = mountTestComponent();
sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
});
it("should render FeedbackView if showFeedbackForm state is true",
function() {
conversationAppStore.setStoreState({showFeedbackForm: true});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView, FeedbackView);
});
it("should dispatch a ShowFeedbackForm action if timestamp is 0",
function() {
conversationAppStore.setStoreState({feedbackTimestamp: 0});
sandbox.stub(dispatcher, "dispatch");
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
it("should set feedback timestamp if delta is > feedback period",
function() {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(setLoopPrefStub);
});
it("should dispatch a ShowFeedbackForm action if delta > feedback period",
function() {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
sandbox.stub(dispatcher, "dispatch");
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
it("should close the window if delta < feedback period", function() {
var feedbackTimestamp = new Date().getTime();
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
ccView = mountTestComponent();
var closeWindowStub = sandbox.stub(ccView, "closeWindow");
ccView.handleCallTerminated();
sinon.assert.calledOnce(closeWindowStub);
});
it("should set the correct timestamp for dateLastSeenSec", function() {
var feedbackTimestamp = new Date().getTime();
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
ccView = mountTestComponent();
var closeWindowStub = sandbox.stub(ccView, "closeWindow");
ccView.handleCallTerminated();
sinon.assert.calledOnce(closeWindowStub);
});
});
});

View File

@ -0,0 +1,100 @@
/* 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/. */
describe("loop.feedbackViews", function() {
"use strict";
var FeedbackView = loop.feedbackViews.FeedbackView;
var l10n = navigator.mozL10n || document.mozL10n;
var TestUtils = React.addons.TestUtils;
var sandbox, mozL10nGet;
beforeEach(function() {
sandbox = sinon.sandbox.create();
mozL10nGet = sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
});
afterEach(function() {
sandbox.restore();
});
describe("FeedbackView", function() {
var openURLStub, getLoopPrefStub, feedbackReceivedStub;
var fakeURL = "fake.form", mozLoop, view;
function mountTestComponent(props) {
props = _.extend({
mozLoop: mozLoop,
onAfterFeedbackReceived: feedbackReceivedStub
}, props);
return TestUtils.renderIntoDocument(
React.createElement(FeedbackView, props));
}
beforeEach(function() {
openURLStub = sandbox.stub();
getLoopPrefStub = sandbox.stub();
feedbackReceivedStub = sandbox.stub();
mozLoop = {
openURL: openURLStub,
getLoopPref: getLoopPrefStub
};
});
afterEach(function() {
view = null;
});
it("should render a feedback view", function() {
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view, FeedbackView);
});
it("should render a button with correct text", function() {
view = mountTestComponent();
sinon.assert.calledWithExactly(mozL10nGet, "feedback_request_button");
});
it("should render a header with correct text", function() {
view = mountTestComponent();
sinon.assert.calledWithExactly(mozL10nGet, "feedback_window_heading");
});
it("should open a new page to the feedback form", function() {
mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
.returns(fakeURL);
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
sinon.assert.calledOnce(openURLStub);
sinon.assert.calledWithExactly(openURLStub, fakeURL);
});
it("should fetch the feedback form URL from the prefs", function() {
mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
.returns(fakeURL);
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
sinon.assert.calledOnce(mozLoop.getLoopPref);
sinon.assert.calledWithExactly(mozLoop.getLoopPref, "feedback.formURL");
});
it("should close the window after opening the form", function() {
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
sinon.assert.calledOnce(feedbackReceivedStub);
});
});
});

View File

@ -47,7 +47,6 @@
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
@ -60,16 +59,15 @@
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/conversationAppStore.js"></script>
<script src="../../content/js/roomStore.js"></script>
<script src="../../content/js/roomViews.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/feedbackViews.js"></script>
<script src="../../content/js/conversation.js"></script>
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
<script src="../../content/js/panel.js"></script>
@ -78,6 +76,7 @@
<script src="conversationAppStore_test.js"></script>
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
<script src="feedbackViews_test.js"></script>
<script src="panel_test.js"></script>
<script src="roomViews_test.js"></script>
<script src="conversationViews_test.js"></script>
@ -97,7 +96,7 @@
describe("Unexpected Warnings Check", function() {
it("should long only the warnings we expect", function() {
chai.expect(caughtWarnings.length).to.eql(30);
chai.expect(caughtWarnings.length).to.eql(27);
});
});

View File

@ -41,7 +41,8 @@ describe("loop.roomViews", function () {
}),
update: sinon.stub().callsArgWith(2, null)
},
telemetryAddValue: sinon.stub()
telemetryAddValue: sinon.stub(),
setLoopPref: sandbox.stub()
};
fakeWindow = {
@ -201,8 +202,7 @@ describe("loop.roomViews", function () {
});
});
it("should dispatch a CopyRoomUrl action when the copy button is " +
"pressed", function() {
it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
@ -290,14 +290,9 @@ describe("loop.roomViews", function () {
});
describe("DesktopRoomConversationView", function() {
var view;
var view, onCallTerminatedStub;
beforeEach(function() {
loop.store.StoreMixin.register({
feedbackStore: new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
})
});
sandbox.stub(dispatcher, "dispatch");
fakeMozLoop.getLoopPref = function(prefName) {
if (prefName == "contextInConversations.enabled") {
@ -305,15 +300,18 @@ describe("loop.roomViews", function () {
}
return "test";
};
onCallTerminatedStub = sandbox.stub();
});
function mountTestComponent() {
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
roomStore: roomStore,
mozLoop: fakeMozLoop,
onCallTerminated: onCallTerminatedStub
}, props);
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.DesktopRoomConversationView, {
dispatcher: dispatcher,
roomStore: roomStore,
mozLoop: fakeMozLoop
}));
React.createElement(loop.roomViews.DesktopRoomConversationView, props));
}
it("should dispatch a setMute action when the audio mute button is pressed",
@ -372,8 +370,7 @@ describe("loop.roomViews", function () {
expect(muteBtn.classList.contains("muted")).eql(true);
});
it("should dispatch a `StartScreenShare` action when sharing is not active " +
"and the screen share button is pressed", function() {
it("should dispatch a `StartScreenShare` action when sharing is not active and the screen share button is pressed", function() {
view = mountTestComponent();
view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
@ -419,8 +416,7 @@ describe("loop.roomViews", function () {
sinon.match.instanceOf(sharedActions.SetupStreamElements));
}
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
"is entered", function() {
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
var component = mountTestComponent();
@ -429,8 +425,7 @@ describe("loop.roomViews", function () {
expectActionDispatched(component);
});
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
"re-entered", function() {
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is re-entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
var component = mountTestComponent();
@ -489,19 +484,19 @@ describe("loop.roomViews", function () {
loop.roomViews.DesktopRoomConversationView);
});
it("should render the FeedbackView if roomState is `ENDED`",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.ENDED,
used: true
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.shared.views.FeedbackView);
it("should call onCallTerminated when the call ended", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.ENDED,
used: true
});
view = mountTestComponent();
// Force a state change so that it triggers componentDidUpdate
view.setState({ foo: "bar" });
sinon.assert.calledOnce(onCallTerminatedStub);
});
it("should display loading spinner when localSrcVideoObject is null",
function() {
activeRoomStore.setStoreState({

View File

@ -152,16 +152,12 @@ class Test1BrowserCall(MarionetteTestCase):
self.switch_to_standalone()
self.check_video(".screen-share-video")
def remote_leave_room_and_verify_feedback(self):
def remote_leave_room(self):
self.switch_to_standalone()
button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
button.click()
# check that the feedback form is displayed
feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
self.switch_to_chatbox()
# check that the local view reverts to the preview mode
self.wait_for_element_displayed(By.CLASS_NAME, "room-invitation-content")
@ -260,7 +256,7 @@ class Test1BrowserCall(MarionetteTestCase):
# which means that the local_check_connection_length below
# verifies that the connection is noted at the time the remote media
# drops, rather than waiting until the window closes.
self.remote_leave_room_and_verify_feedback()
self.remote_leave_room()
self.local_check_connection_length_noted()

View File

@ -3,6 +3,7 @@
"globals": {
// General test items.
"add_task": false,
"BrowserTestUtils": true,
"Cc": true,
"Ci": true,
"Cr": true,
@ -24,7 +25,6 @@
"promiseOAuthGetRegistration": false,
"promiseOAuthParamsSetup": false,
"promiseObserverNotified": false,
"promiseTabLoadEvent": false,
"promiseWaitForCondition": false,
"resetFxA": true,
// Loop specific items

View File

@ -23,8 +23,7 @@ add_task(function* test_mozLoop_getSelectedTabMetadata() {
Assert.strictEqual(metadata.title, "", "Title should be empty for about:blank");
Assert.deepEqual(metadata.previews, [], "No previews available for about:blank");
let tab = gBrowser.selectedTab = gBrowser.addTab();
yield promiseTabLoadEvent(tab, "about:home");
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
metadata = yield promiseGetMetadata();
Assert.strictEqual(metadata.url, null, "URL should be empty for about:home");
@ -34,12 +33,11 @@ add_task(function* test_mozLoop_getSelectedTabMetadata() {
// elements with chrome:// srcs, which show up as null in metadata.previews.
Assert.deepEqual(metadata.previews.filter(e => e), [], "No previews available for about:home");
gBrowser.removeTab(tab);
yield BrowserTestUtils.removeTab(tab);
});
add_task(function* test_mozLoop_getSelectedTabMetadata_defaultIcon() {
let tab = gBrowser.selectedTab = gBrowser.addTab();
yield promiseTabLoadEvent(tab, "http://example.com/");
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
let metadata = yield promiseGetMetadata();
Assert.strictEqual(metadata.url, "http://example.com/", "URL should match");
@ -47,5 +45,5 @@ add_task(function* test_mozLoop_getSelectedTabMetadata_defaultIcon() {
Assert.ok(metadata.title, "Title should be set");
Assert.deepEqual(metadata.previews, [], "No previews available");
gBrowser.removeTab(tab);
yield BrowserTestUtils.removeTab(tab);
});

View File

@ -15,9 +15,9 @@ add_task(loadLoopPanel);
add_task(function* test_mozLoop_pluralStrings() {
Assert.ok(gMozLoopAPI, "mozLoop should exist");
var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
"This window will close in {{countdown}} seconds");
var strings = JSON.parse(gMozLoopAPI.getStrings("import_contacts_success_message"));
Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
"This window will close in {{countdown}} second");
"{{total}} contact was successfully imported.");
Assert.equal(gMozLoopAPI.getPluralForm(3, strings.textContent),
"{{total}} contacts were successfully imported.");
});

View File

@ -46,10 +46,10 @@ function promiseWindowIdReceivedNewTab(handlersParam = []) {
}));
});
let createdTab = gBrowser.selectedTab = gBrowser.addTab();
let createdTab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
createdTabs.push(createdTab);
promiseHandlers.push(promiseTabLoadEvent(createdTab, "about:mozilla"));
promiseHandlers.push(BrowserTestUtils.browserLoaded(createdTab.linkedBrowser));
return Promise.all(promiseHandlers);
}

View File

@ -207,52 +207,6 @@ function promiseOAuthGetRegistration(baseURL) {
});
}
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @param [optional] event
* The load event type to wait for. Defaults to "load".
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url, eventType="load") {
return new Promise((resolve, reject) => {
info("Wait tab event: " + eventType);
function handle(event) {
if (event.originalTarget != tab.linkedBrowser.contentDocument ||
event.target.location.href == "about:blank" ||
(url && event.target.location.href != url)) {
info("Skipping spurious '" + eventType + "'' event" +
" for " + event.target.location.href);
return;
}
clearTimeout(timeout);
tab.linkedBrowser.removeEventListener(eventType, handle, true);
info("Tab event received: " + eventType);
resolve(event);
}
let timeout = setTimeout(() => {
if (tab.linkedBrowser) {
tab.linkedBrowser.removeEventListener(eventType, handle, true);
}
reject(new Error("Timed out while waiting for a '" + eventType + "'' event"));
}, 30000);
tab.linkedBrowser.addEventListener(eventType, handle, true, true);
if (url) {
tab.linkedBrowser.loadURI(url);
}
});
}
function getLoopString(stringID) {
return MozLoopServiceInternal.localizedStrings.get(stringID);
}

View File

@ -566,30 +566,6 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#feedbackComplete", function() {
it("should set the room state to READY", function() {
store.setStoreState({
roomState: ROOM_STATES.ENDED,
used: true
});
store.feedbackComplete(new sharedActions.FeedbackComplete());
expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
});
it("should reset the 'used' state", function() {
store.setStoreState({
roomState: ROOM_STATES.ENDED,
used: true
});
store.feedbackComplete(new sharedActions.FeedbackComplete());
expect(store.getStoreState().used).eql(false);
});
});
describe("#videoDimensionsChanged", function() {
it("should not contain any video dimensions at the very start", function() {
expect(store.getStoreState()).eql(store.getInitialStoreState());

View File

@ -1,184 +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/. */
describe("loop.FeedbackAPIClient", function() {
"use strict";
var expect = chai.expect;
var sandbox,
fakeXHR,
requests = [];
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function (xhr) {
requests.push(xhr);
};
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should require a baseUrl setting", function() {
expect(function() {
return new loop.FeedbackAPIClient();
}).to.Throw(/required 'baseUrl'/);
});
it("should require a product setting", function() {
expect(function() {
return new loop.FeedbackAPIClient("http://fake", {});
}).to.Throw(/required 'product'/);
});
});
describe("constructed", function() {
var client;
beforeEach(function() {
client = new loop.FeedbackAPIClient("http://fake/feedback", {
product: "Hello",
version: "42b1"
});
});
describe("#send", function() {
it("should send happy feedback data", function() {
var feedbackData = {
happy: true,
description: "Happy User"
};
client.send(feedbackData, function(){});
expect(requests).to.have.length.of(1);
expect(requests[0].url).to.be.equal("http://fake/feedback");
expect(requests[0].method).to.be.equal("POST");
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.happy).eql(true);
expect(parsed.description).eql("Happy User");
});
it("should send sad feedback data", function() {
var feedbackData = {
happy: false,
category: "confusing"
};
client.send(feedbackData, function(){});
expect(requests).to.have.length.of(1);
expect(requests[0].url).to.be.equal("http://fake/feedback");
expect(requests[0].method).to.be.equal("POST");
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.happy).eql(false);
expect(parsed.product).eql("Hello");
expect(parsed.category).eql("confusing");
expect(parsed.description).eql("Sad User");
});
it("should send formatted feedback data", function() {
client.send({
happy: false,
category: "other",
description: "it's far too awesome!"
}, function(){});
expect(requests).to.have.length.of(1);
expect(requests[0].url).eql("http://fake/feedback");
expect(requests[0].method).eql("POST");
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.happy).eql(false);
expect(parsed.product).eql("Hello");
expect(parsed.category).eql("other");
expect(parsed.description).eql("it's far too awesome!");
});
it("should send product information", function() {
client.send({product: "Hello"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.product).eql("Hello");
});
it("should send platform information when provided", function() {
client.send({platform: "Windows 8"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.platform).eql("Windows 8");
});
it("should send channel information when provided", function() {
client.send({channel: "beta"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.channel).eql("beta");
});
it("should send version information when provided", function() {
client.send({version: "42b1"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.version).eql("42b1");
});
it("should send user_agent information when provided", function() {
client.send({user_agent: "MOZAGENT"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.user_agent).eql("MOZAGENT");
});
it("should send url information when provided", function() {
client.send({url: "http://fake.invalid"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.url).eql("http://fake.invalid");
});
it("should throw on invalid feedback data", function() {
expect(function() {
client.send("invalid data", function(){});
}).to.Throw(/Invalid/);
});
it("should throw on unsupported field name", function() {
expect(function() {
client.send({bleh: "bah"}, function(){});
}).to.Throw(/Unsupported/);
});
it("should call passed callback on success", function() {
var cb = sandbox.spy();
var fakeResponseData = {description: "confusing"};
client.send({category: "confusing"}, cb);
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(fakeResponseData));
sinon.assert.calledOnce(cb);
sinon.assert.calledWithExactly(cb, null, fakeResponseData);
});
it("should call passed callback on error", function() {
var cb = sandbox.spy();
var fakeErrorData = {error: true};
client.send({category: "confusing"}, cb);
requests[0].respond(400, {"Content-Type": "application/json"},
JSON.stringify(fakeErrorData));
sinon.assert.calledOnce(cb);
sinon.assert.calledWithExactly(cb, sinon.match(function(err) {
return /Bad Request/.test(err);
}));
});
});
});
});

View File

@ -1,121 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.store.FeedbackStore", function () {
"use strict";
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var sandbox, dispatcher, store, feedbackClient;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
feedbackClient = new loop.FeedbackAPIClient("http://invalid", {
product: "Loop"
});
store = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should throw an error if feedbackClient is missing", function() {
expect(function() {
new loop.store.FeedbackStore(dispatcher);
}).to.Throw(/feedbackClient/);
});
it("should set the store to the INIT feedback state", function() {
var fakeStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
expect(fakeStore.getStoreState("feedbackState"))
.eql(FEEDBACK_STATES.INIT);
});
});
describe("#requireFeedbackDetails", function() {
it("should transition to DETAILS state", function() {
store.requireFeedbackDetails(new sharedActions.RequireFeedbackDetails());
expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.DETAILS);
});
});
describe("#sendFeedback", function() {
var sadFeedbackData = {
happy: false,
category: "fakeCategory",
description: "fakeDescription"
};
beforeEach(function() {
store.requireFeedbackDetails();
});
it("should send feedback data over the feedback client", function() {
sandbox.stub(feedbackClient, "send");
store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
sinon.assert.calledOnce(feedbackClient.send);
sinon.assert.calledWithMatch(feedbackClient.send, sadFeedbackData);
});
it("should transition to PENDING state", function() {
sandbox.stub(feedbackClient, "send");
store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.PENDING);
});
it("should transition to SENT state on successful submission", function(done) {
sandbox.stub(feedbackClient, "send", function(data, cb) {
cb(null);
});
store.once("change:feedbackState", function() {
expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.SENT);
done();
});
store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
});
it("should transition to FAILED state on failed submission", function(done) {
sandbox.stub(feedbackClient, "send", function(data, cb) {
cb(new Error("failed"));
});
store.once("change:feedbackState", function() {
expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.FAILED);
done();
});
store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
});
});
describe("feedbackComplete", function() {
it("should reset the store state", function() {
store.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
store.feedbackComplete();
expect(store.getStoreState()).eql({
feedbackState: FEEDBACK_STATES.INIT
});
});
});
});

View File

@ -1,191 +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/. */
describe("loop.shared.views.FeedbackView", function() {
"use strict";
var expect = chai.expect;
var l10n = navigator.mozL10n || document.mozL10n;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var sandbox, comp, dispatcher, fakeFeedbackClient, feedbackStore;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
fakeFeedbackClient = {send: sandbox.stub()};
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: fakeFeedbackClient
});
loop.store.StoreMixin.register({feedbackStore: feedbackStore});
comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.FeedbackView));
});
afterEach(function() {
sandbox.restore();
});
// local test helpers
function clickHappyFace(component) {
var happyFace = component.getDOMNode().querySelector(".face-happy");
TestUtils.Simulate.click(happyFace);
}
function clickSadFace(component) {
var sadFace = component.getDOMNode().querySelector(".face-sad");
TestUtils.Simulate.click(sadFace);
}
function fillSadFeedbackForm(component, category, text) {
TestUtils.Simulate.change(
component.getDOMNode().querySelector("[value='" + category + "']"));
if (text) {
TestUtils.Simulate.change(
component.getDOMNode().querySelector("[name='description']"), {
target: {value: "fake reason"}
});
}
}
function submitSadFeedbackForm(component, category, text) {
TestUtils.Simulate.submit(component.getDOMNode().querySelector("form"));
}
describe("Happy feedback", function() {
it("should dispatch a SendFeedback action", function() {
var dispatch = sandbox.stub(dispatcher, "dispatch");
clickHappyFace(comp);
sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
happy: true,
category: "",
description: ""
}));
});
it("should thank the user once feedback data is sent", function() {
feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
.eql(null);
});
it("should not display the countdown text if noCloseText is true", function() {
comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.FeedbackView, {
noCloseText: true
}));
expect(comp.getDOMNode().querySelector(".info.thank-you")).eql(null);
});
});
describe("Sad feedback", function() {
it("should bring the user to feedback form when clicking on the sad face",
function() {
clickSadFace(comp);
expect(comp.getDOMNode().querySelectorAll("form")).not.eql(null);
});
it("should render a back button", function() {
feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
.not.eql(null);
});
it("should reset the view when clicking the back button", function() {
feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
TestUtils.Simulate.click(
comp.getDOMNode().querySelector("button.fx-embedded-btn-back"));
expect(comp.getDOMNode().querySelector(".faces")).not.eql(null);
});
it("should disable the form submit button when no category is chosen",
function() {
clickSadFace(comp);
expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
});
it("should disable the form submit button when the 'other' category is " +
"chosen but no description has been entered yet",
function() {
clickSadFace(comp);
fillSadFeedbackForm(comp, "other");
expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
});
it("should enable the form submit button when the 'other' category is " +
"chosen and a description is entered",
function() {
clickSadFace(comp);
fillSadFeedbackForm(comp, "other", "fake");
expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
});
it("should enable the form submit button once a predefined category is " +
"chosen",
function() {
clickSadFace(comp);
fillSadFeedbackForm(comp, "confusing");
expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
});
it("should send feedback data when the form is submitted", function() {
var dispatch = sandbox.stub(dispatcher, "dispatch");
feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
fillSadFeedbackForm(comp, "confusing");
submitSadFeedbackForm(comp);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
happy: false,
category: "confusing",
description: ""
}));
});
it("should send feedback data when user has entered a custom description",
function() {
clickSadFace(comp);
fillSadFeedbackForm(comp, "other", "fake reason");
submitSadFeedbackForm(comp);
sinon.assert.calledOnce(fakeFeedbackClient.send);
sinon.assert.calledWith(fakeFeedbackClient.send, {
happy: false,
category: "other",
description: "fake reason"
});
});
it("should thank the user when feedback data has been sent", function() {
fakeFeedbackClient.send = function(data, cb) {
cb();
};
clickSadFace(comp);
fillSadFeedbackForm(comp, "confusing");
submitSadFeedbackForm(comp);
expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
});
});
});

View File

@ -52,7 +52,6 @@
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/crypto.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
@ -62,11 +61,9 @@
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
@ -75,14 +72,11 @@
<script src="crypto_test.js"></script>
<script src="views_test.js"></script>
<script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script>
<script src="feedbackViews_test.js"></script>
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
<script src="activeRoomStore_test.js"></script>
<script src="fxOSActiveRoomStore_test.js"></script>
<script src="conversationStore_test.js"></script>
<script src="feedbackStore_test.js"></script>
<script src="otSdkDriver_test.js"></script>
<script src="store_test.js"></script>
<script src="textChatStore_test.js"></script>

View File

@ -49,7 +49,6 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
@ -57,11 +56,9 @@
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../standalone/content/js/multiplexGum.js"></script>
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
@ -88,7 +85,7 @@
describe("Unexpected Warnings Check", function() {
it("should long only the warnings we expect", function() {
chai.expect(caughtWarnings.length).to.eql(15);
chai.expect(caughtWarnings.length).to.eql(11);
});
});

View File

@ -15,7 +15,7 @@ describe("loop.standaloneRoomViews", function() {
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
var sandbox, dispatcher, activeRoomStore, dispatch;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -28,12 +28,8 @@ describe("loop.standaloneRoomViews", function() {
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {}
});
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
feedbackStore: feedbackStore,
textChatStore: textChatStore
});
@ -501,38 +497,6 @@ describe("loop.standaloneRoomViews", function() {
});
});
describe("Feedback", function() {
beforeEach(function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.ENDED,
used: true
});
});
it("should display a feedback form when the user leaves the room",
function() {
expect(view.getDOMNode().querySelector(".faces")).not.eql(null);
});
it("should dispatch a `FeedbackComplete` action after feedback is sent",
function() {
feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
sandbox.clock.tick(
loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS * 1000 + 1000);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.FeedbackComplete());
});
it("should NOT display a feedback form if the room has not been used",
function() {
activeRoomStore.setStoreState({used: false});
expect(view.getDOMNode().querySelector(".faces")).eql(null);
});
});
describe("Mute", function() {
it("should render a local avatar if video is muted",
function() {

View File

@ -23,11 +23,6 @@ describe("loop.webapp", function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
notifications = new sharedModels.NotificationCollection();
loop.store.StoreMixin.register({
feedbackStore: new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
})
});
stubGetPermsAndCacheMedia = sandbox.stub(
loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
@ -54,7 +49,6 @@ describe("loop.webapp", function() {
describe("#init", function() {
beforeEach(function() {
sandbox.stub(React, "render");
loop.config.feedbackApiUrl = "http://fake.invalid";
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
});
@ -1077,10 +1071,6 @@ describe("loop.webapp", function() {
it("should render a ConversationView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
});
it("should render a FeedbackView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
});
});
describe("PromoteFirefoxView", function() {

View File

@ -42,7 +42,6 @@
<script src="../content/shared/libs/jquery-2.1.4.js"></script>
<script src="../content/shared/libs/lodash-3.9.3.js"></script>
<script src="../content/shared/libs/backbone-1.2.1.js"></script>
<script src="../content/shared/js/feedbackApiClient.js"></script>
<script src="../content/shared/js/actions.js"></script>
<script src="../content/shared/js/utils.js"></script>
<script src="../content/shared/js/models.js"></script>
@ -55,10 +54,9 @@
<script src="../content/shared/js/roomStates.js"></script>
<script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../content/shared/js/activeRoomStore.js"></script>
<script src="../content/shared/js/feedbackStore.js"></script>
<script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/feedbackViews.js"></script>
<script src="../content/shared/js/textChatStore.js"></script>
<script src="../content/js/feedbackViews.js"></script>
<script src="../content/shared/js/textChatView.js"></script>
<script src="../content/js/roomStore.js"></script>
<script src="../content/js/roomViews.js"></script>

View File

@ -32,13 +32,12 @@
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var FeedbackView = loop.shared.views.FeedbackView;
var FeedbackView = loop.feedbackViews.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
var TextChatView = loop.shared.views.chat.TextChatView;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
// Local helpers
@ -76,14 +75,6 @@
var dispatcher = new loop.Dispatcher();
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
}
);
var mockSDK = _.extend({
sendTextChatMessage: function(message) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
@ -281,9 +272,6 @@
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: stageFeedbackApiClient
});
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
@ -354,7 +342,6 @@
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStore,
feedbackStore: feedbackStore,
textChatStore: textChatStore
});
@ -861,24 +848,13 @@
),
React.createElement(Section, {name: "FeedbackView"},
React.createElement("p", {className: "note"},
React.createElement("strong", null, "Note:"), " For the useable demo, you can access submitted data at ",
React.createElement("a", {href: "https://input.allizom.org/"}, "input.allizom.org"), "."
React.createElement("p", {className: "note"}
),
React.createElement(Example, {dashed: true,
style: {width: "300px", height: "272px"},
summary: "Default (useable demo)"},
React.createElement(FeedbackView, {feedbackStore: feedbackStore})
),
React.createElement(Example, {dashed: true,
style: {width: "300px", height: "272px"},
summary: "Detailed form"},
React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.DETAILS, feedbackStore: feedbackStore})
),
React.createElement(Example, {dashed: true,
style: {width: "300px", height: "272px"},
summary: "Thank you!"},
React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.SENT, feedbackStore: feedbackStore})
React.createElement(FeedbackView, {mozLoop: {},
onAfterFeedbackReceived: function() {}})
)
),
@ -926,6 +902,7 @@
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
roomState: ROOM_STATES.INIT,
roomStore: invitationRoomStore})
)
@ -943,6 +920,7 @@
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
roomStore: desktopRoomStoreLoading})
@ -956,6 +934,7 @@
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
roomStore: roomStore})
@ -970,6 +949,7 @@
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomStore: desktopLocalFaceMuteRoomStore})
)
@ -983,6 +963,7 @@
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
roomStore: desktopRemoteFaceMuteRoomStore})
)
)
@ -1171,20 +1152,6 @@
)
),
React.createElement(FramedExample, {cssClass: "standalone",
dashed: true,
height: 483,
summary: "Standalone room conversation (feedback)",
width: 644},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
activeRoomStore: endedRoomStore,
dispatcher: dispatcher,
feedbackStore: feedbackStore,
isFirefox: false})
)
),
React.createElement(FramedExample, {cssClass: "standalone",
dashed: true,
height: 483,
@ -1315,7 +1282,7 @@
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
var expectedWarningsCount = 24;
var expectedWarningsCount = 23;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +

View File

@ -32,13 +32,12 @@
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var FeedbackView = loop.shared.views.FeedbackView;
var FeedbackView = loop.feedbackViews.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
var TextChatView = loop.shared.views.chat.TextChatView;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
// Local helpers
@ -76,14 +75,6 @@
var dispatcher = new loop.Dispatcher();
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
}
);
var mockSDK = _.extend({
sendTextChatMessage: function(message) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
@ -281,9 +272,6 @@
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: stageFeedbackApiClient
});
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
@ -354,7 +342,6 @@
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStore,
feedbackStore: feedbackStore,
textChatStore: textChatStore
});
@ -862,23 +849,12 @@
<Section name="FeedbackView">
<p className="note">
<strong>Note:</strong> For the useable demo, you can access submitted data at&nbsp;
<a href="https://input.allizom.org/">input.allizom.org</a>.
</p>
<Example dashed={true}
style={{width: "300px", height: "272px"}}
summary="Default (useable demo)">
<FeedbackView feedbackStore={feedbackStore} />
</Example>
<Example dashed={true}
style={{width: "300px", height: "272px"}}
summary="Detailed form">
<FeedbackView feedbackState={FEEDBACK_STATES.DETAILS} feedbackStore={feedbackStore} />
</Example>
<Example dashed={true}
style={{width: "300px", height: "272px"}}
summary="Thank you!">
<FeedbackView feedbackState={FEEDBACK_STATES.SENT} feedbackStore={feedbackStore}/>
<FeedbackView mozLoop={{}}
onAfterFeedbackReceived={function() {}} />
</Example>
</Section>
@ -926,6 +902,7 @@
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
roomState={ROOM_STATES.INIT}
roomStore={invitationRoomStore} />
</div>
@ -943,6 +920,7 @@
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
roomStore={desktopRoomStoreLoading} />
@ -956,6 +934,7 @@
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
roomStore={roomStore} />
@ -970,6 +949,7 @@
<DesktopRoomConversationView
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomStore={desktopLocalFaceMuteRoomStore} />
</div>
@ -983,6 +963,7 @@
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
roomStore={desktopRemoteFaceMuteRoomStore} />
</div>
</FramedExample>
@ -1171,20 +1152,6 @@
</div>
</FramedExample>
<FramedExample cssClass="standalone"
dashed={true}
height={483}
summary="Standalone room conversation (feedback)"
width={644}>
<div className="standalone">
<StandaloneRoomView
activeRoomStore={endedRoomStore}
dispatcher={dispatcher}
feedbackStore={feedbackStore}
isFirefox={false} />
</div>
</FramedExample>
<FramedExample cssClass="standalone"
dashed={true}
height={483}
@ -1315,7 +1282,7 @@
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
var expectedWarningsCount = 24;
var expectedWarningsCount = 23;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +

View File

@ -89,8 +89,8 @@
<vbox>
<hbox align="center">
<checkbox id="privacyDoNotTrackCheckbox"
label="&dntTrackingNotOkay.label2;"
accesskey="&dntTrackingNotOkay.accesskey;"
label="&dntTrackingNotOkay2.label;"
accesskey="&dntTrackingNotOkay2.accesskey;"
preference="privacy.donottrackheader.enabled"/>
<label id="doNotTrackInfo"
class="text-link"
@ -103,8 +103,8 @@
<hbox align="center">
<checkbox id="trackingProtection"
preference="privacy.trackingprotection.enabled"
accesskey="&trackingProtection.accesskey;"
label="&trackingProtection.label;" />
accesskey="&trackingProtection2.accesskey;"
label="&trackingProtection2.label;" />
<label id="trackingProtectionLearnMore"
class="text-link"
value="&trackingProtectionLearnMore.label;"/>
@ -114,8 +114,8 @@
<hbox align="center">
<checkbox id="trackingProtectionPBM"
preference="privacy.trackingprotection.pbmode.enabled"
accesskey="&trackingProtectionPBM.accesskey;"
label="&trackingProtectionPBM.label;" />
accesskey="&trackingProtectionPBM2.accesskey;"
label="&trackingProtectionPBM2.label;" />
<label id="trackingProtectionPBMLearnMore"
class="text-link"
value="&trackingProtectionPBMLearnMore.label;"/>

View File

@ -131,7 +131,6 @@ let gSyncPane = {
_toggleComputerNameControls: function(editMode) {
let textbox = document.getElementById("fxaSyncComputerName");
textbox.className = editMode ? "" : "plain";
textbox.disabled = !editMode;
document.getElementById("fxaChangeDeviceName").hidden = editMode;
document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;

View File

@ -322,30 +322,27 @@
</hbox>
</groupbox>
<groupbox>
<vbox>
<caption>
<label accesskey="&syncDeviceName.accesskey;"
control="fxaSyncComputerName">
&fxaSyncDeviceName.label;
</label>
</caption>
<hbox id="fxaDeviceName">
<hbox flex="1">
<textbox id="fxaSyncComputerName" class="plain"
disabled="true" flex="1"/>
</hbox>
<hbox>
<button id="fxaChangeDeviceName"
label="&changeSyncDeviceName.label;"/>
<button id="fxaCancelChangeDeviceName"
label="&cancelChangeSyncDeviceName.label;"
hidden="true"/>
<button id="fxaSaveChangeDeviceName"
label="&saveChangeSyncDeviceName.label;"
hidden="true"/>
</hbox>
<caption>
<label accesskey="&syncDeviceName.accesskey;"
control="fxaSyncComputerName">
&fxaSyncDeviceName.label;
</label>
</caption>
<hbox id="fxaDeviceName">
<hbox flex="1">
<textbox id="fxaSyncComputerName" disabled="true" flex="1"/>
</hbox>
</vbox>
<hbox>
<button id="fxaChangeDeviceName"
label="&changeSyncDeviceName.label;"/>
<button id="fxaCancelChangeDeviceName"
label="&cancelChangeSyncDeviceName.label;"
hidden="true"/>
<button id="fxaSaveChangeDeviceName"
label="&saveChangeSyncDeviceName.label;"
hidden="true"/>
</hbox>
</hbox>
</groupbox>
<spacer flex="1"/>
<vbox id="tosPP-small">

View File

@ -96,8 +96,8 @@
<hbox align="center">
<checkbox id="trackingProtection"
preference="privacy.trackingprotection.enabled"
accesskey="&trackingProtection.accesskey;"
label="&trackingProtection.label;" />
accesskey="&trackingProtection2.accesskey;"
label="&trackingProtection2.label;" />
<image id="trackingProtectionImage"
src="chrome://browser/skin/bad-content-blocked-16.png"/>
</hbox>
@ -111,8 +111,8 @@
<vbox>
<hbox align="center">
<checkbox id="privacyDoNotTrackCheckbox"
label="&dntTrackingNotOkay.label2;"
accesskey="&dntTrackingNotOkay.accesskey;"
label="&dntTrackingNotOkay2.label;"
accesskey="&dntTrackingNotOkay2.accesskey;"
preference="privacy.donottrackheader.enabled"/>
</hbox>
<hbox align="center"

View File

@ -16,6 +16,7 @@ function test() {
status: 'VALID',
args: {
nocache: { value: false },
safemode: { value: false },
}
},
},
@ -27,6 +28,31 @@ function test() {
status: 'VALID',
args: {
nocache: { value: true },
safemode: { value: false },
}
},
},
{
setup: 'restart --safemode',
check: {
input: 'restart --safemode',
markup: 'VVVVVVVVVVVVVVVVVV',
status: 'VALID',
args: {
nocache: { value: false },
safemode: { value: true },
}
},
},
{
setup: 'restart --safemode --nocache',
check: {
input: 'restart --safemode --nocache',
markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
status: 'VALID',
args: {
nocache: { value: true },
safemode: { value: true },
}
},
},

View File

@ -140,6 +140,7 @@ skip-if = os == 'linux' # Bug 1172120
[browser_profiler_tree-view-10.js]
[browser_profiler_tree-view-11.js]
[browser_timeline-filters-01.js]
skip-if = true # Bug 1176370
[browser_timeline-filters-02.js]
[browser_timeline-waterfall-background.js]
[browser_timeline-waterfall-generic.js]

View File

@ -11,6 +11,7 @@ support-files =
doc_content_stylesheet_xul.css
doc_copystyles.css
doc_copystyles.html
doc_custom.html
doc_filter.html
doc_frame_script.js
doc_keyframeanimation.html
@ -83,6 +84,7 @@ skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work wit
[browser_ruleview_cubicbezier-appears-on-swatch-click.js]
[browser_ruleview_cubicbezier-commit-on-ENTER.js]
[browser_ruleview_cubicbezier-revert-on-ESC.js]
[browser_ruleview_custom.js]
[browser_ruleview_edit-property-commit.js]
[browser_ruleview_edit-property-computed.js]
[browser_ruleview_edit-property-increments.js]

View File

@ -0,0 +1,80 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = TEST_URL_ROOT + "doc_custom.html";
// Test the display of custom declarations in the rule-view
add_task(function*() {
yield addTab(TEST_URI);
let {inspector, view} = yield openRuleView();
yield simpleCustomOverride(inspector, view);
yield importantCustomOverride(inspector, view);
yield disableCustomOverride(inspector, view);
});
function* simpleCustomOverride(inspector, view) {
yield selectNode("#testidSimple", inspector);
let elementStyle = view._elementStyle;
let idRule = elementStyle.rules[1];
let idProp = idRule.textProps[0];
is(idProp.name, "--background-color",
"First ID prop should be --background-color");
ok(!idProp.overridden, "ID prop should not be overridden.");
let classRule = elementStyle.rules[2];
let classProp = classRule.textProps[0];
is(classProp.name, "--background-color",
"First class prop should be --background-color");
ok(classProp.overridden, "Class property should be overridden.");
// Override --background-color by changing the element style.
let elementRule = elementStyle.rules[0];
elementRule.createProperty("--background-color", "purple", "");
yield elementRule._applyingModifications;
let elementProp = elementRule.textProps[0];
is(classProp.name, "--background-color",
"First element prop should now be --background-color");
ok(!elementProp.overridden,
"Element style property should not be overridden");
ok(idProp.overridden, "ID property should be overridden");
ok(classProp.overridden, "Class property should be overridden");
}
function* importantCustomOverride(inspector, view) {
yield selectNode("#testidImportant", inspector);
let elementStyle = view._elementStyle;
let idRule = elementStyle.rules[1];
let idProp = idRule.textProps[0];
ok(idProp.overridden, "Not-important rule should be overridden.");
let classRule = elementStyle.rules[2];
let classProp = classRule.textProps[0];
ok(!classProp.overridden, "Important rule should not be overridden.");
}
function* disableCustomOverride(inspector, view) {
yield selectNode("#testidDisable", inspector);
let elementStyle = view._elementStyle;
let idRule = elementStyle.rules[1];
let idProp = idRule.textProps[0];
idProp.setEnabled(false);
yield idRule._applyingModifications;
let classRule = elementStyle.rules[2];
let classProp = classRule.textProps[0];
ok(!classProp.overridden,
"Class prop should not be overridden after id prop was disabled.");
}

View File

@ -0,0 +1,33 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<html>
<head>
<style>
#testidSimple {
--background-color: blue;
}
.testclassSimple {
--background-color: green;
}
.testclassImportant {
--background-color: green !important;
}
#testidImportant {
--background-color: blue;
}
#testidDisable {
--background-color: blue;
}
.testclassDisable {
--background-color: green;
}
</style>
</head>
<body>
<div id="testidSimple" class="testclassSimple">Styled Node</div>
<div id="testidImportant" class="testclassImportant">Styled Node</div>
<div id="testidDisable" class="testclassDisable">Styled Node</div>
</body>
</html>

View File

@ -274,28 +274,13 @@ legal_text_privacy = Privacy Notice
powered_by_beforeLogo=Powered by
powered_by_afterLogo=
feedback_call_experience_heading2=How was your conversation?
feedback_thank_you_heading=Thank you for your feedback!
feedback_category_list_heading=What made you sad?
feedback_category_audio_quality=Audio quality
feedback_category_video_quality=Video quality
feedback_category_was_disconnected=Was disconnected
feedback_category_confusing2=Confusing controls
feedback_category_other2=Other
feedback_custom_category_text_placeholder=What went wrong?
feedback_submit_button=Submit
feedback_back_button=Back
## LOCALIZATION NOTE (feedback_window_will_close_in2):
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
feedback_window_heading=How was your conversation?
feedback_request_button=Leave Feedback
help_label=Help

View File

@ -4,15 +4,15 @@
<!ENTITY tracking.label "Tracking">
<!ENTITY dntTrackingNotOkay.label2 "Tell sites that I do not want to be tracked">
<!ENTITY dntTrackingNotOkay.accesskey "n">
<!ENTITY trackingProtection.label "Prevent sites from tracking me">
<!ENTITY trackingProtection.accesskey "m">
<!ENTITY dntTrackingNotOkay2.label "Tell sites that I do not want to be tracked.">
<!ENTITY dntTrackingNotOkay2.accesskey "n">
<!ENTITY doNotTrackInfo.label "Learn More">
<!ENTITY trackingProtection2.label "Prevent sites from tracking me.">
<!ENTITY trackingProtection2.accesskey "m">
<!ENTITY trackingProtectionLearnMore.label "Learn more">
<!ENTITY trackingProtectionPBM.label "Prevent sites from tracking my online activity in Private Windows">
<!ENTITY trackingProtectionPBM.accesskey "y">
<!ENTITY trackingProtectionPBM2.label "Prevent sites from tracking my online activity in Private Windows.">
<!ENTITY trackingProtectionPBM2.accesskey "y">
<!ENTITY trackingProtectionPBMLearnMore.label "Learn more">
<!ENTITY doNotTrackInfo.label "Learn More">
<!ENTITY history.label "History">

View File

@ -396,11 +396,6 @@ description > html|a {
margin-left: 0px;
}
#fxaSyncComputerName.plain {
background-color: transparent;
opacity: 1;
}
#tosPP-small-ToS {
margin-bottom: 1em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

View File

@ -144,7 +144,7 @@
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close);
}
#titlebar-close:hover {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highlight);
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
}
/* the 12px image renders a 10px icon, and the 10px upscaled gets rounded to 12.5, which

View File

@ -1630,6 +1630,15 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-moz-image-region: rect(0, 16px, 11px, 0);
padding: 0 3px;
width: 22px;
height: 11px;
}
@media (min-resolution: 1.1dppx) {
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
-moz-image-region: rect(0, 32px, 22px, 0);
}
}
@media not all and (-moz-os-version: windows-vista),
@ -1640,6 +1649,12 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
-moz-image-region: rect(11px, 16px, 22px, 0);
}
@media (min-resolution: 1.1dppx) {
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
-moz-image-region: rect(22px, 32px, 44px, 0);
}
}
.ac-comment[selected="true"],
.ac-url-text[selected="true"],
.ac-action-text[selected="true"] {

View File

@ -1,7 +1,7 @@
<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
g {
stroke: ButtonText;
@ -21,9 +21,13 @@
display: initial;
}
.highlight > g {
[id$="-highlight"] > g {
stroke: HighlightText;
}
[id$="-white"] > g {
stroke: #fff;
}
</style>
<g id="close">
<line x1="1" y1="1" x2="11" y2="11"/>
@ -39,8 +43,13 @@
<rect x="1.5" y="3.5" width="7" height="7"/>
<polyline points="3.5,3.5 3.5,1.5 10.5,1.5 10.5,8.5 8.5,8.5"/>
</g>
<use id="close-highlight" class="highlight" xlink:href="#close"/>
<use id="maximize-highlight" class="highlight" xlink:href="#maximize"/>
<use id="minimize-highlight" class="highlight" xlink:href="#minimize"/>
<use id="restore-highlight" class="highlight" xlink:href="#restore"/>
<use id="close-highlight" xlink:href="#close"/>
<use id="maximize-highlight" xlink:href="#maximize"/>
<use id="minimize-highlight" xlink:href="#minimize"/>
<use id="restore-highlight" xlink:href="#restore"/>
<use id="close-white" xlink:href="#close"/>
<use id="maximize-white" xlink:href="#maximize"/>
<use id="minimize-white" xlink:href="#minimize"/>
<use id="restore-white" xlink:href="#restore"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -240,4 +240,18 @@
padding-left: 15px;
padding-right: 15px;
}
/* Force white caption buttons for the dark theme on Windows 10 */
:root[devtoolstheme="dark"] #titlebar-min {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-white);
}
:root[devtoolstheme="dark"] #titlebar-max {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-white);
}
#main-window[devtoolstheme="dark"][sizemode="maximized"] #titlebar-max {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-white);
}
:root[devtoolstheme="dark"] #titlebar-close {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
}
}

View File

@ -22,6 +22,8 @@ browser.jar:
#endif
skin/classic/browser/aboutTabCrashed.css (../shared/aboutTabCrashed.css)
skin/classic/browser/actionicon-tab.png
skin/classic/browser/actionicon-tab@2x.png
skin/classic/browser/actionicon-tab-XPVista7.png
skin/classic/browser/addons/addon-install-blocked.svg (../shared/addons/addon-install-blocked.svg)
skin/classic/browser/addons/addon-install-confirm.svg (../shared/addons/addon-install-confirm.svg)
skin/classic/browser/addons/addon-install-downloading.svg (../shared/addons/addon-install-downloading.svg)
@ -645,6 +647,7 @@ browser.jar:
% override chrome://browser/skin/preferences/saveFile.png chrome://browser/skin/preferences/saveFile-XP.png os=WINNT osversion<6
% override chrome://browser/skin/tabbrowser/tab-separator.png chrome://browser/skin/tabbrowser/tab-separator-XP.png os=WINNT osversion<6
% override chrome://browser/skin/actionicon-tab.png chrome://browser/skin/actionicon-tab-XPVista7.png os=WINNT osversion<=6.1
% override chrome://browser/skin/sync-horizontalbar.png chrome://browser/skin/sync-horizontalbar-XPVista7.png os=WINNT osversion<=6.1
% override chrome://browser/skin/sync-horizontalbar@2x.png chrome://browser/skin/sync-horizontalbar-XPVista7@2x.png os=WINNT osversion<=6.1
% override chrome://browser/skin/syncProgress-horizontalbar.png chrome://browser/skin/syncProgress-horizontalbar-XPVista7.png os=WINNT osversion<=6.1

View File

@ -33,6 +33,9 @@ to date and optimally configured for a better, more productive experience
when working on Mozilla projects.
Please run `mach mercurial-setup` now.
Note: `mach mercurial-setup` does not make any changes without prompting
you first.
'''.strip()
OLD_MERCURIAL_TOOLS = '''
@ -46,6 +49,9 @@ more productive experience when working on Mozilla projects.
Please run `mach mercurial-setup` now.
Reminder: `mach mercurial-setup` does not make any changes without
prompting you first.
To avoid this message in the future, run `mach mercurial-setup` once a month.
Or, schedule `mach mercurial-setup --update-only` to run automatically in
the background at least once a month.

View File

@ -623,12 +623,17 @@ inDOMUtils::GetSubpropertiesForCSSProperty(const nsAString& aProperty,
nsCSSProperty propertyID =
nsCSSProps::LookupProperty(aProperty, nsCSSProps::eEnabledForAllContent);
if (propertyID == eCSSProperty_UNKNOWN ||
propertyID == eCSSPropertyExtra_variable) {
if (propertyID == eCSSProperty_UNKNOWN) {
return NS_ERROR_FAILURE;
}
nsTArray<nsString> array;
if (propertyID == eCSSPropertyExtra_variable) {
*aValues = static_cast<char16_t**>(moz_xmalloc(sizeof(char16_t*)));
(*aValues)[0] = ToNewUnicode(aProperty);
*aLength = 1;
return NS_OK;
}
if (!nsCSSProps::IsShorthand(propertyID)) {
*aValues = static_cast<char16_t**>(moz_xmalloc(sizeof(char16_t*)));
(*aValues)[0] = ToNewUnicode(nsCSSProps::GetStringValue(propertyID));

View File

@ -14,7 +14,6 @@ support-files =
[test_bug856317.html]
[test_bug877690.html]
[test_bug1006595.html]
[test_bug1046140.html]
[test_color_to_rgba.html]
[test_css_property_is_valid.html]
[test_get_all_style_sheets.html]

View File

@ -32,6 +32,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1006595
arraysEqual(displaySubProps, [ "color" ],
"'color' subproperties");
var varProps = utils.getSubpropertiesForCSSProperty("--foo");
arraysEqual(varProps, ["--foo"], "'--foo' subproperties");
ok(utils.cssPropertyIsShorthand("padding"), "'padding' is a shorthand")
ok(!utils.cssPropertyIsShorthand("color"), "'color' is not a shorthand")

View File

@ -1,34 +0,0 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1046140
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1046140</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
var utils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]
.getService(SpecialPowers.Ci.inIDOMUtils);
try {
utils.getSubpropertiesForCSSProperty("--foo");
ok(false, "expected an exception");
} catch(e) {
ok(true, "getSubpropertiesForCSSProperty throws when passed a CSS variable");
}
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1046140">Mozilla Bug 1046140</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>

View File

@ -86,20 +86,10 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
remoteTabsEntry = null;
}
// On tablets, we go [...|History|Recent Tabs|Synced Tabs].
// On phones, we go [Synced Tabs|Recent Tabs|History|...].
if (HardwareUtils.isTablet()) {
panelConfigs.add(historyEntry);
panelConfigs.add(recentTabsEntry);
if (remoteTabsEntry != null) {
panelConfigs.add(remoteTabsEntry);
}
} else {
panelConfigs.add(0, historyEntry);
panelConfigs.add(0, recentTabsEntry);
if (remoteTabsEntry != null) {
panelConfigs.add(0, remoteTabsEntry);
}
panelConfigs.add(historyEntry);
panelConfigs.add(recentTabsEntry);
if (remoteTabsEntry != null) {
panelConfigs.add(remoteTabsEntry);
}
return new State(panelConfigs, true);

View File

@ -191,8 +191,6 @@ public class HomePager extends ViewPager {
setCurrentItem(index, true);
}
});
} else if (child instanceof HomePagerTabStrip) {
mTabStrip = child;
}
super.addView(child, index, params);

View File

@ -102,15 +102,24 @@ public class SearchEngineBar extends TwoWayView
final int searchEngineCount = adapter.getCount() - 1;
if (searchEngineCount > 0) {
final float availableWidthPerContainer = (getMeasuredWidth() - labelContainerWidth) / searchEngineCount;
final int availableWidth = getMeasuredWidth() - labelContainerWidth;
final double searchEnginesToDisplay;
final int desiredIconContainerSize = (int) Math.max(
availableWidthPerContainer,
minIconContainerWidth
);
if (searchEngineCount * minIconContainerWidth <= availableWidth) {
// All search engines fit int: So let's just display all.
searchEnginesToDisplay = searchEngineCount;
} else {
// If only (n) search engines fit into the available space then display (n - 0.5): The last search
// engine will be cut-off to show ability to scroll this view
if (desiredIconContainerSize != iconContainerWidth) {
iconContainerWidth = desiredIconContainerSize;
searchEnginesToDisplay = Math.floor(availableWidth / minIconContainerWidth) - 0.5;
}
// Use all available width and spread search engine icons
final int availableWidthPerContainer = (int) (availableWidth / searchEnginesToDisplay);
if (availableWidthPerContainer != iconContainerWidth) {
iconContainerWidth = availableWidthPerContainer;
adapter.notifyDataSetChanged();
}
}

View File

@ -28,21 +28,25 @@ class TabMenuStripLayout extends LinearLayout
private HomePager.OnTitleClickListener onTitleClickListener;
private Drawable strip;
private View selectedView;
private TextView selectedView;
// Data associated with the scrolling of the strip drawable.
private View toTab;
private View fromTab;
private int fromPosition;
private int toPosition;
private float progress;
// This variable is used to predict the direction of scroll.
private float prevProgress;
private int tabContentStart;
TabMenuStripLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip);
final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1);
tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabContentStart, 0);
a.recycle();
if (stripResId != -1) {
@ -55,6 +59,14 @@ class TabMenuStripLayout extends LinearLayout
void onAddPagerView(String title) {
final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
button.setText(title.toUpperCase());
button.setTextColor(getResources().getColorStateList(R.color.tab_text_color));
if (getChildCount() == 0) {
button.setPadding(button.getPaddingLeft() + tabContentStart,
button.getPaddingTop(),
button.getPaddingRight(),
button.getPaddingBottom());
}
addView(button);
button.setOnClickListener(new ViewClickListener(getChildCount() - 1));
@ -62,7 +74,12 @@ class TabMenuStripLayout extends LinearLayout
}
void onPageSelected(final int position) {
selectedView = getChildAt(position);
if (selectedView != null) {
selectedView.setTextColor(getResources().getColorStateList(R.color.tab_text_color));
}
selectedView = (TextView) getChildAt(position);
selectedView.setTextColor(getResources().getColor(R.color.placeholder_grey));
// Callback to measure and draw the strip after the view is visible.
ViewTreeObserver vto = selectedView.getViewTreeObserver();
@ -73,7 +90,7 @@ class TabMenuStripLayout extends LinearLayout
selectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
if (strip != null) {
strip.setBounds(selectedView.getLeft(),
strip.setBounds(selectedView.getLeft() + (position == 0 ? tabContentStart : 0),
selectedView.getTop(),
selectedView.getRight(),
selectedView.getBottom());
@ -103,10 +120,25 @@ class TabMenuStripLayout extends LinearLayout
final int toTabLeft = toTab.getLeft();
final int toTabRight = toTab.getRight();
strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)),
0,
(int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
getHeight());
// The first tab has a padding applied (tabContentStart). We don't want the 'strip' to jump around so we remove
// this padding slowly (modifier) when scrolling to or from the first tab.
final int modifier;
if (fromPosition == 0 && toPosition == 1) {
// Slowly remove extra padding (tabContentStart) based on scroll progress
modifier = (int) (tabContentStart * (1 - progress));
} else if (fromPosition == 1 && toPosition == 0) {
// Slowly add extra padding (tabContentStart) based on scroll progress
modifier = (int) (tabContentStart * progress);
} else {
// We are not scrolling tab 0 in any way, no modifier needed
modifier = 0;
}
strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)) + modifier,
0,
(int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
getHeight());
invalidate();
}
@ -123,15 +155,18 @@ class TabMenuStripLayout extends LinearLayout
final float currProgress = position + positionOffset;
if (prevProgress > currProgress) {
toTab = getChildAt(position);
fromTab = getChildAt(position + 1);
toPosition = position;
fromPosition = position + 1;
progress = 1 - positionOffset;
} else {
toTab = getChildAt(position + 1);
fromTab = getChildAt(position);
toPosition = position + 1;
fromPosition = position;
progress = positionOffset;
}
toTab = getChildAt(toPosition);
fromTab = getChildAt(fromPosition);
prevProgress = currProgress;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:color="@color/placeholder_grey" />
<item android:color="@color/panel_tab_text_normal"/>
</selector>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- This file is used to include the home pager in gecko app
layout based on screen size -->
<org.mozilla.gecko.home.HomePager xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:id="@+id/home_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
android:layout_height="@dimen/tabs_strip_height"
android:background="@color/about_page_header_grey"
android:layout_gravity="top"
gecko:strip="@drawable/home_tab_menu_strip"/>
</org.mozilla.gecko.home.HomePager>

View File

@ -13,12 +13,11 @@
android:layout_height="match_parent"
android:background="@android:color/white">
<org.mozilla.gecko.home.HomePagerTabStrip android:layout_width="match_parent"
android:layout_height="@dimen/tabs_strip_height"
android:layout_gravity="top"
android:gravity="center_vertical"
android:background="@color/about_page_header_grey"
gecko:tabIndicatorColor="@color/fennec_ui_orange"
android:textAppearance="@style/TextAppearance.Widget.HomePagerTabStrip"/>
<org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
android:layout_height="@dimen/tabs_strip_height"
android:background="@color/about_page_header_grey"
android:layout_gravity="top"
gecko:strip="@drawable/home_tab_menu_strip"
gecko:tabContentStart="72dp" />
</org.mozilla.gecko.home.HomePager>

View File

@ -169,6 +169,7 @@
<declare-styleable name="TabMenuStrip">
<attr name="strip" format="reference"/>
<attr name="tabContentStart" format="dimension" />
</declare-styleable>
<declare-styleable name="TabPanelBackButton">

View File

@ -116,6 +116,7 @@
<color name="panel_image_item_background">#D1D9E1</color>
<color name="panel_icon_item_title_background">#32000000</color>
<color name="panel_tab_text_normal">#FFBFBFBF</color>
<!-- Swipe to refresh colors for dynamic panel -->
<color name="swipe_refresh_orange">#FFFFC26C</color>

View File

@ -129,7 +129,7 @@
<dimen name="tab_thumbnail_width">160dp</dimen>
<dimen name="tabs_panel_indicator_width">60dp</dimen>
<dimen name="tabs_panel_button_width">48dp</dimen>
<dimen name="tabs_strip_height">40dp</dimen>
<dimen name="tabs_strip_height">48dp</dimen>
<dimen name="tabs_strip_button_width">100dp</dimen>
<dimen name="tabs_strip_button_padding">18dp</dimen>
<dimen name="tabs_strip_shadow_size">1dp</dimen>

View File

@ -8,6 +8,7 @@ import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
import java.util.Arrays;
import java.util.List;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.R;
@ -15,7 +16,6 @@ import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.tests.UITestContext;
import org.mozilla.gecko.tests.helpers.WaitHelper;
import org.mozilla.gecko.util.HardwareUtils;
import android.os.Build;
import android.support.v4.view.ViewPager;
@ -31,26 +31,14 @@ import com.jayway.android.robotium.solo.Solo;
public class AboutHomeComponent extends BaseComponent {
private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
// TODO: Having a specific ordering of panels is prone to fail and thus temporary.
// Hopefully the work in bug 940565 will alleviate the need for these enums.
// Explicit ordering of HomePager panels on a phone.
private static final PanelType[] PANEL_ORDERING_PHONE = {
PanelType.REMOTE_TABS,
PanelType.RECENT_TABS,
PanelType.HISTORY,
PanelType.TOP_SITES,
PanelType.BOOKMARKS,
PanelType.READING_LIST,
};
private static final PanelType[] PANEL_ORDERING_TABLET = {
private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
PanelType.TOP_SITES,
PanelType.BOOKMARKS,
PanelType.READING_LIST,
PanelType.HISTORY,
PanelType.RECENT_TABS,
PanelType.REMOTE_TABS,
};
PanelType.REMOTE_TABS
);
// The percentage of the panel to swipe between 0 and 1. This value was set through
// testing: 0.55f was tested on try and fails on armv6 devices.
@ -78,7 +66,7 @@ public class AboutHomeComponent extends BaseComponent {
public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
assertVisible();
final int expectedPanelIndex = getPanelIndexForDevice(expectedPanel);
final int expectedPanelIndex = PANEL_ORDERING.indexOf(expectedPanel);
fAssertEquals("The current HomePager panel is " + expectedPanel,
expectedPanelIndex, getHomePagerView().getCurrentItem());
return this;
@ -172,15 +160,14 @@ public class AboutHomeComponent extends BaseComponent {
// The panel on the left is a lower index and vice versa.
final int unboundedPanelIndex = panelIndex + (panelDirection == Solo.LEFT ? -1 : 1);
final int panelCount = getPanelOrderingForDevice().length;
final int maxPanelIndex = panelCount - 1;
final int maxPanelIndex = PANEL_ORDERING.size() - 1;
final int expectedPanelIndex = Math.min(Math.max(0, unboundedPanelIndex), maxPanelIndex);
waitForPanelIndex(expectedPanelIndex);
}
private void waitForPanelIndex(final int expectedIndex) {
final String panelName = getPanelOrderingForDevice()[expectedIndex].name();
final String panelName = PANEL_ORDERING.get(expectedIndex).name();
WaitHelper.waitFor("HomePager " + panelName + " panel", new Condition() {
@Override
@ -190,23 +177,6 @@ public class AboutHomeComponent extends BaseComponent {
});
}
/**
* Get the expected panel index for the given PanelType on this device. Different panel
* orderings are expected on tables vs. phones.
*/
private int getPanelIndexForDevice(final PanelType panelType) {
PanelType[] panelOrdering = getPanelOrderingForDevice();
return Arrays.asList(panelOrdering).indexOf(panelType);
}
/**
* Get an array of PanelType objects ordered as we want the panels to be ordered on this device.
*/
public static PanelType[] getPanelOrderingForDevice() {
return HardwareUtils.isTablet() ? PANEL_ORDERING_TABLET : PANEL_ORDERING_PHONE;
}
/**
* Navigate directly to a built-in panel by its panel type.
* <p>
@ -219,7 +189,7 @@ public class AboutHomeComponent extends BaseComponent {
*/
public AboutHomeComponent navigateToBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(panelType));
final int expectedPanelIndex = getPanelIndexForDevice(panelType);
final int expectedPanelIndex = PANEL_ORDERING.indexOf(panelType);
waitForPanelIndex(expectedPanelIndex);
return this;
}

View File

@ -153,6 +153,7 @@ skip-if = android_version == "10" || android_version == "18"
# Used for Talos, please don't use in mochitest
#[testCheck2.java]
#[testCheck3.java] # and the autophone version
# Using UITest
#[testAboutHomePageNavigation.java] # see bug 947550, bug 979038 and bug 977952

View File

@ -0,0 +1,69 @@
/* 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/. */
package org.mozilla.gecko.tests;
import org.json.JSONObject;
public class testCheck3 extends PixelTest {
@Override
protected Type getTestType() {
return Type.TALOS;
}
public void testCheck3() {
String url = getAbsoluteUrl("/facebook.com/www.facebook.com/barackobama.html");
// Enable double-tap zooming
JSONObject jsonPref = new JSONObject();
try {
jsonPref.put("name", "browser.ui.zoom.force-user-scalable");
jsonPref.put("type", "bool");
jsonPref.put("value", true);
setPreferenceAndWaitForChange(jsonPref);
} catch (Exception ex) {
mAsserter.ok(false, "exception in testCheck3", ex.toString());
}
blockForGeckoReady();
loadAndPaint(url);
mDriver.setupScrollHandling();
/*
* for this test, we load the timecube page, and replay a recorded sequence of events
* that is a user panning/zooming around the page. specific things in the sequence
* include:
* - scroll on one axis followed by scroll on another axis
* - pinch zoom (in and out)
* - double-tap zoom (in and out)
* - multi-fling panning with different velocities on each fling
*
* this checkerboarding metric is going to be more of a "functional" style test than
* a "unit" style test; i.e. it covers a little bit of a lot of things to measure
* overall performance, but doesn't really allow identifying which part is slow.
*/
MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(),
mDriver.getGeckoWidth(), mDriver.getGeckoHeight());
float completeness = 0.0f;
mDriver.startCheckerboardRecording();
// replay the events
try {
mer.replayEvents(getAsset("testcheck2-motionevents"));
// give it some time to draw any final frames
Thread.sleep(1000);
completeness = mDriver.stopCheckerboardRecording();
} catch (Exception e) {
e.printStackTrace();
mAsserter.ok(false, "Exception while replaying events", e.toString());
}
mAsserter.dumpLog("__start_report" + completeness + "__end_report");
System.out.println("Completeness score: " + completeness);
long msecs = System.currentTimeMillis();
mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp");
}
}

View File

@ -683,33 +683,24 @@ function Snapshot({xpcom, childProcesses, probes}) {
* Communication with other processes
*/
let Process = {
// `true` once communications have been initialized
_initialized: false,
// the message manager
_loader: null,
// a counter used to match responses to requests
_idcounter: 0,
_loader: null,
/**
* If we are in a child process, return `null`.
* Otherwise, return the global parent process message manager
* and load the script to connect to children processes.
*/
get loader() {
if (this._initialized) {
return this._loader;
}
this._initialized = true;
this._loader = Services.ppmm;
if (!this._loader) {
// We are in a child process.
if (isContent) {
return null;
}
this._loader.loadProcessScript("resource://gre/modules/PerformanceStats-content.js",
if (this._loader) {
return this._loader;
}
Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceStats-content.js",
true/*including future processes*/);
return this._loader;
return this._loader = Services.ppmm;
},
/**
@ -751,21 +742,12 @@ let Process = {
let collected = [];
let deferred = PromiseUtils.defer();
// The content script may be loaded more than once (bug 1184115).
// To avoid double-responses, we keep track of who has already responded.
// Note that we could it on the other end, at the expense of implementing
// an additional .jsm just for that purpose.
let responders = new Set();
let observer = function({data, target}) {
if (data.id != id) {
// Collision between two collections,
// ignore the other one.
return;
}
if (responders.has(target)) {
return;
}
responders.add(target);
if (data.data) {
collected.push(data.data)
}

View File

@ -31,13 +31,21 @@ exports.items = [
runAt: "client",
name: "restart",
description: l10n.lookupFormat("restartBrowserDesc", [ BRAND_SHORT_NAME ]),
params: [
{
name: "nocache",
type: "boolean",
description: l10n.lookup("restartBrowserNocacheDesc")
}
],
params: [{
group: l10n.lookup("restartBrowserGroupOptions"),
params: [
{
name: "nocache",
type: "boolean",
description: l10n.lookup("restartBrowserNocacheDesc")
},
{
name: "safemode",
type: "boolean",
description: l10n.lookup("restartBrowserSafemodeDesc")
}
]
}],
returnType: "string",
exec: function Restart(args, context) {
let canceled = Cc["@mozilla.org/supports-PRBool;1"]
@ -52,10 +60,17 @@ exports.items = [
Services.appinfo.invalidateCachesOnRestart();
}
// restart
Cc["@mozilla.org/toolkit/app-startup;1"]
.getService(Ci.nsIAppStartup)
.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
const appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
.getService(Ci.nsIAppStartup);
if (args.safemode) {
// restart in safemode
appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
} else {
// restart normally
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
}
return l10n.lookupFormat("restartBrowserRestarting", [ BRAND_SHORT_NAME ]);
}
}

View File

@ -282,6 +282,15 @@ restartBrowserRequestCancelled=Restart request cancelled by user.
# The argument (%1$S) is the browser name.
restartBrowserRestarting=Restarting %1$S…
# LOCALIZATION NOTE (restartBrowserGroupOptions) A label for the optional options of
# the restart command.
restartBrowserGroupOptions=Options
# LOCALIZATION NOTE (restartBrowserSafemodeDesc) A very short string to
# describe the 'safemode' parameter to the 'restart' command, which is
# displayed in a dialog when the user is using this command.
restartBrowserSafemodeDesc=Enables Safe Mode upon restart
# LOCALIZATION NOTE (inspectDesc) A very short description of the 'inspect'
# command. See inspectManual for a fuller description of what it does. This
# string is designed to be shown in a menu alongside the command name, which

View File

@ -186,7 +186,12 @@ class MercurialConfig(object):
return None, None
b = self._c['bugzilla']
return b.get('username', None), b.get('password', None)
return (
b.get('username', None),
b.get('password', None),
b.get('userid', None),
b.get('cookie', None),
)
def set_bugzilla_credentials(self, username, password):
b = self._c.setdefault('bugzilla', {})

View File

@ -8,6 +8,7 @@ import difflib
import errno
import os
import shutil
import stat
import sys
import which
import subprocess
@ -53,11 +54,15 @@ MISSING_USERNAME = '''
You don't have a username defined in your Mercurial config file. In order to
send patches to Mozilla, you'll need to attach a name and email address. If you
aren't comfortable giving us your full name, pseudonames are acceptable.
(Relevant config option: ui.username)
'''.strip()
BAD_DIFF_SETTINGS = '''
Mozilla developers produce patches in a standard format, but your Mercurial is
not configured to produce patches in that format.
(Relevant config options: diff.git, diff.showfunc, diff.unified)
'''.strip()
MQ_INFO = '''
@ -67,6 +72,8 @@ alternative to the recommended bookmark-based development workflow.
If you are a newcomer to Mercurial or are coming from Git, it is
recommended to avoid mq.
(Relevant config option: extensions.mq)
Would you like to activate the mq extension
'''.strip()
@ -76,6 +83,8 @@ bzexport that makes it easy to upload patches from the command line via the
|hg bzexport| command. More info is available at
https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/bzexport/README
(Relevant config option: extensions.bzexport)
Would you like to activate bzexport
'''.strip()
@ -84,6 +93,8 @@ The mqext extension adds a number of features, including automatically committin
changes to your mq patch queue. More info is available at
https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/mqext/README.txt
(Relevant config option: extensions.mqext)
Would you like to activate mqext
'''.strip()
@ -93,6 +104,8 @@ The qimportbz extension
import patches from Bugzilla using a friendly bz:// URL handler. e.g.
|hg qimport bz://123456|.
(Relevant config option: extensions.qimportbz)
Would you like to activate qimportbz
'''.strip()
@ -123,8 +136,12 @@ Various extensions make use of your Bugzilla credentials to interface with
Bugzilla to enrich your development experience.
Bugzilla credentials are optional. If you do not provide them, associated
functionality will not be enabled or you will be prompted for your
Bugzilla credentials when they are needed.
functionality will not be enabled, we will attempt to find a Bugzilla cookie
from a Firefox profile, or you will be prompted for your Bugzilla credentials
when they are needed.
Your Bugzilla credentials will be stored in *PLAIN TEXT* in your hgrc config
file. If this is not wanted, do not enter your credentials.
'''.lstrip()
BZPOST_MINIMUM_VERSION = LooseVersion('3.1')
@ -133,6 +150,8 @@ BZPOST_INFO = '''
The bzpost extension automatically records the URLs of pushed commits to
referenced Bugzilla bugs after push.
(Relevant config option: extensions.bzpost)
Would you like to activate bzpost
'''.strip()
@ -157,6 +176,8 @@ The firefoxtree extension is *strongly* recommended if you:
a) aggregate multiple Firefox repositories into a single local repo
b) perform head/bookmark-based development (as opposed to mq)
(Relevant config option: extensions.firefoxtree)
Would you like to activate firefoxtree
'''.strip()
@ -168,6 +189,8 @@ try syntax and pushes it to the try server. The extension is intended
to be used in concert with other tools generating try syntax so that
they can push to try without depending on mq or other workarounds.
(Relevant config option: extensions.push-to-try)
Would you like to activate push-to-try
'''.strip()
@ -178,9 +201,17 @@ The bundleclone extension makes cloning faster and saves server resources.
We highly recommend you activate this extension.
(Relevant config option: extensions.bundleclone)
Would you like to activate bundleclone
'''.strip()
FILE_PERMISSIONS_WARNING = '''
Your hgrc file is currently readable by others.
Sensitive information such as your Bugzilla credentials could be
stolen if others have access to this file/machine.
'''.strip()
class MercurialSetupWizard(object):
"""Command-line wizard to help users configure Mercurial."""
@ -272,8 +303,10 @@ class MercurialSetupWizard(object):
print('Fixed patch settings.')
print('')
self.prompt_native_extension(c, 'progress',
'Would you like to see progress bars during Mercurial operations')
# Progress is built into core and enabled by default in Mercurial 3.5.
if hg_version < LooseVersion('3.5'):
self.prompt_native_extension(c, 'progress',
'Would you like to see progress bars during Mercurial operations')
self.prompt_native_extension(c, 'color',
'Would you like Mercurial to colorize output to your terminal')
@ -289,8 +322,6 @@ class MercurialSetupWizard(object):
self.prompt_native_extension(c, 'mq', MQ_INFO)
self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO)
if 'reviewboard' not in c.extensions:
if hg_version < REVIEWBOARD_MINIMUM_VERSION:
print(REVIEWBOARD_INCOMPATIBLE % REVIEWBOARD_MINIMUM_VERSION)
@ -303,6 +334,8 @@ class MercurialSetupWizard(object):
'projects',
path=p)
self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO)
if hg_version >= BZPOST_MINIMUM_VERSION:
self.prompt_external_extension(c, 'bzpost', BZPOST_INFO)
@ -335,17 +368,18 @@ class MercurialSetupWizard(object):
print('')
if 'reviewboard' in c.extensions or 'bzpost' in c.extensions:
bzuser, bzpass = c.get_bugzilla_credentials()
bzuser, bzpass, bzuserid, bzcookie = c.get_bugzilla_credentials()
if not bzuser or not bzpass:
if (not bzuser or not bzpass) and (not bzuserid or not bzcookie):
print(MISSING_BUGZILLA_CREDENTIALS)
if not bzuser:
bzuser = self._prompt('What is your Bugzilla email address?',
# Don't prompt for username if cookie is set.
if not bzuser and not bzuserid:
bzuser = self._prompt('What is your Bugzilla email address? (optional)',
allow_empty=True)
if bzuser and not bzpass:
bzpass = self._prompt('What is your Bugzilla password?',
bzpass = self._prompt('What is your Bugzilla password? (optional)',
allow_empty=True)
if bzuser or bzpass:
@ -402,6 +436,19 @@ class MercurialSetupWizard(object):
c.write(sys.stdout)
return 1
# Config file may contain sensitive content, such as passwords.
# Prompt to remove global permissions.
mode = os.stat(config_path).st_mode
if mode & (stat.S_IRWXG | stat.S_IRWXO):
print(FILE_PERMISSIONS_WARNING)
if self._prompt_yn('Remove permissions for others to read '
'your hgrc file'):
# We don't care about sticky and set UID bits because this is
# a regular file.
mode = mode & stat.S_IRWXU
print('Changing permissions of %s' % config_path)
os.chmod(config_path, mode)
print(FINISHED)
return 0

View File

@ -67,4 +67,8 @@ class VersionControlCommands(object):
wizard = MercurialSetupWizard(self._context.state_dir)
result = wizard.run(map(os.path.expanduser, config_paths))
if result:
print('(despite the failure, mach will not nag you to run '
'`mach mercurial-setup`)')
return result

View File

@ -18,7 +18,6 @@ function test() {
ok(aResponse.tabs[0].gcliActor, "gcliActor set");
ok(aResponse.tabs[0].styleEditorActor, "styleEditorActor set");
ok(aResponse.tabs[0].inspectorActor, "inspectorActor set");
ok(aResponse.tabs[0].traceActor, "traceActor set");
ok(aResponse.deviceActor, "deviceActor set");
client.close(() => {

View File

@ -999,6 +999,12 @@ nsIWidget* nsWindow::GetParent(void)
return GetParentWindow(false);
}
static int32_t RoundDown(double aDouble)
{
return aDouble > 0 ? static_cast<int32_t>(floor(aDouble)) :
static_cast<int32_t>(ceil(aDouble));
}
float nsWindow::GetDPI()
{
HDC dc = ::GetDC(mWnd);
@ -3711,7 +3717,9 @@ nsWindow::UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries)
if (IsWin10OrLater() && mCustomNonClient && mSizeMode == nsSizeMode_Normal) {
RECT rect;
::GetWindowRect(mWnd, &rect);
clearRegion.Or(clearRegion, nsIntRect(0, 0, rect.right - rect.left, 1.0));
// We want 1 pixel of border for every whole 100% of scaling
double borderSize = RoundDown(GetDefaultScale().scale);
clearRegion.Or(clearRegion, nsIntRect(0, 0, rect.right - rect.left, borderSize));
}
if (!IsWin10OrLater()) {
for (size_t i = 0; i < aThemeGeometries.Length(); i++) {
@ -6457,12 +6465,6 @@ bool nsWindow::OnTouch(WPARAM wParam, LPARAM lParam)
return true;
}
static int32_t RoundDown(double aDouble)
{
return aDouble > 0 ? static_cast<int32_t>(floor(aDouble)) :
static_cast<int32_t>(ceil(aDouble));
}
// Gesture event processing. Handles WM_GESTURE events.
bool nsWindow::OnGesture(WPARAM wParam, LPARAM lParam)
{