mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
cd1b84f5fd
@ -1575,10 +1575,14 @@ pref("loop.enabled", false);
|
||||
|
||||
pref("loop.server", "https://loop.services.mozilla.com");
|
||||
pref("loop.seenToS", "unseen");
|
||||
pref("loop.legal.ToS_url", "https://accounts.firefox.com/legal/terms");
|
||||
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
|
||||
pref("loop.do_not_disturb", false);
|
||||
pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
|
||||
pref("loop.retry_delay.start", 60000);
|
||||
pref("loop.retry_delay.limit", 300000);
|
||||
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
|
||||
pref("loop.feedback.product", "Loop");
|
||||
|
||||
// serverURL to be assigned by services team
|
||||
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
|
||||
|
@ -2,7 +2,7 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const Cu = Components.utils;
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource:///modules/webrtcUI.jsm");
|
||||
|
||||
@ -129,7 +129,12 @@ let PositionHandler = {
|
||||
adjustPosition: function() {
|
||||
if (!this.positionCustomized) {
|
||||
// Center the window horizontally on the screen (not the available area).
|
||||
window.moveTo((screen.width - document.documentElement.clientWidth) / 2, 0);
|
||||
// Until we have moved the window to y=0, 'screen.width' may give a value
|
||||
// for a secondary screen, so use values from the screen manager instead.
|
||||
let width = {};
|
||||
Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager)
|
||||
.primaryScreen.GetRectDisplayPix({}, {}, width, {});
|
||||
window.moveTo((width.value - document.documentElement.clientWidth) / 2, 0);
|
||||
} else {
|
||||
// This will ensure we're at y=0.
|
||||
this.setXPosition(window.screenX);
|
||||
|
@ -26,7 +26,7 @@
|
||||
window.OTProperties.cssURL = window.OTProperties.assetURL + 'css/ot.css';
|
||||
</script>
|
||||
<script type="text/javascript" src="loop/libs/sdk.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/react-0.10.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/react-0.11.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
@ -35,6 +35,7 @@
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
|
||||
<script type="text/javascript" src="loop/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
||||
|
@ -116,30 +116,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call ended view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
var EndedCallView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<p>',
|
||||
' <button class="btn btn-info" data-l10n-id="close_window"></button>',
|
||||
'</p>'
|
||||
].join("")),
|
||||
|
||||
className: "call-ended",
|
||||
|
||||
events: {
|
||||
"click button": "closeWindow"
|
||||
},
|
||||
|
||||
closeWindow: function(event) {
|
||||
event.preventDefault();
|
||||
// XXX For now, we just close the window.
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation router.
|
||||
*
|
||||
@ -155,8 +131,8 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"call/accept": "accept",
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended",
|
||||
"call/declineAndBlock": "declineAndBlock"
|
||||
"call/declineAndBlock": "declineAndBlock",
|
||||
"call/feedback": "feedback"
|
||||
},
|
||||
|
||||
/**
|
||||
@ -170,7 +146,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* @override {loop.shared.router.BaseConversationRouter.endCall}
|
||||
*/
|
||||
endCall: function() {
|
||||
this.navigate("call/ended", {trigger: true});
|
||||
this.navigate("call/feedback", {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -180,7 +156,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* by the router from the URL.
|
||||
*/
|
||||
incoming: function(loopVersion) {
|
||||
window.navigator.mozLoop.startAlerting();
|
||||
navigator.mozLoop.startAlerting();
|
||||
this._conversation.set({loopVersion: loopVersion});
|
||||
this._conversation.once("accept", function() {
|
||||
this.navigate("call/accept", {trigger: true});
|
||||
@ -200,7 +176,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* Accepts an incoming call.
|
||||
*/
|
||||
accept: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
navigator.mozLoop.stopAlerting();
|
||||
this._conversation.initiate({
|
||||
client: new loop.Client(),
|
||||
outgoing: false
|
||||
@ -211,7 +187,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* Declines an incoming call.
|
||||
*/
|
||||
decline: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
navigator.mozLoop.stopAlerting();
|
||||
// XXX For now, we just close the window
|
||||
window.close();
|
||||
},
|
||||
@ -223,7 +199,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* after a callUrl is received
|
||||
*/
|
||||
declineAndBlock: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
navigator.mozLoop.stopAlerting();
|
||||
var token = navigator.mozLoop.getLoopCharPref('loopToken');
|
||||
var client = new loop.Client();
|
||||
client.deleteCallUrl(token, function(error) {
|
||||
@ -254,10 +230,17 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
},
|
||||
|
||||
/**
|
||||
* XXX: load a view with a close button for now?
|
||||
* Call has ended, display a feedback form.
|
||||
*/
|
||||
ended: function() {
|
||||
this.loadView(new EndedCallView());
|
||||
feedback: function() {
|
||||
document.title = mozL10n.get("call_has_ended");
|
||||
|
||||
this.loadReactComponent(sharedViews.FeedbackView({
|
||||
feedbackApiClient: new loop.FeedbackAPIClient({
|
||||
baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
|
||||
product: navigator.mozLoop.getLoopCharPref("feedback.product")
|
||||
})
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@ -267,7 +250,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
function init() {
|
||||
// Do the initial L10n setup, we do this before anything
|
||||
// else to ensure the L10n environment is setup correctly.
|
||||
mozL10n.initialize(window.navigator.mozLoop);
|
||||
mozL10n.initialize(navigator.mozLoop);
|
||||
|
||||
document.title = mozL10n.get("incoming_call_title");
|
||||
|
||||
@ -280,7 +263,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
|
||||
return {
|
||||
ConversationRouter: ConversationRouter,
|
||||
EndedCallView: EndedCallView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
};
|
||||
|
@ -116,30 +116,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call ended view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
var EndedCallView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<p>',
|
||||
' <button class="btn btn-info" data-l10n-id="close_window"></button>',
|
||||
'</p>'
|
||||
].join("")),
|
||||
|
||||
className: "call-ended",
|
||||
|
||||
events: {
|
||||
"click button": "closeWindow"
|
||||
},
|
||||
|
||||
closeWindow: function(event) {
|
||||
event.preventDefault();
|
||||
// XXX For now, we just close the window.
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation router.
|
||||
*
|
||||
@ -155,8 +131,8 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"call/accept": "accept",
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended",
|
||||
"call/declineAndBlock": "declineAndBlock"
|
||||
"call/declineAndBlock": "declineAndBlock",
|
||||
"call/feedback": "feedback"
|
||||
},
|
||||
|
||||
/**
|
||||
@ -170,7 +146,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* @override {loop.shared.router.BaseConversationRouter.endCall}
|
||||
*/
|
||||
endCall: function() {
|
||||
this.navigate("call/ended", {trigger: true});
|
||||
this.navigate("call/feedback", {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -180,7 +156,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* by the router from the URL.
|
||||
*/
|
||||
incoming: function(loopVersion) {
|
||||
window.navigator.mozLoop.startAlerting();
|
||||
navigator.mozLoop.startAlerting();
|
||||
this._conversation.set({loopVersion: loopVersion});
|
||||
this._conversation.once("accept", function() {
|
||||
this.navigate("call/accept", {trigger: true});
|
||||
@ -200,7 +176,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* Accepts an incoming call.
|
||||
*/
|
||||
accept: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
navigator.mozLoop.stopAlerting();
|
||||
this._conversation.initiate({
|
||||
client: new loop.Client(),
|
||||
outgoing: false
|
||||
@ -211,7 +187,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* Declines an incoming call.
|
||||
*/
|
||||
decline: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
navigator.mozLoop.stopAlerting();
|
||||
// XXX For now, we just close the window
|
||||
window.close();
|
||||
},
|
||||
@ -223,7 +199,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* after a callUrl is received
|
||||
*/
|
||||
declineAndBlock: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
navigator.mozLoop.stopAlerting();
|
||||
var token = navigator.mozLoop.getLoopCharPref('loopToken');
|
||||
var client = new loop.Client();
|
||||
client.deleteCallUrl(token, function(error) {
|
||||
@ -254,10 +230,17 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
},
|
||||
|
||||
/**
|
||||
* XXX: load a view with a close button for now?
|
||||
* Call has ended, display a feedback form.
|
||||
*/
|
||||
ended: function() {
|
||||
this.loadView(new EndedCallView());
|
||||
feedback: function() {
|
||||
document.title = mozL10n.get("call_has_ended");
|
||||
|
||||
this.loadReactComponent(sharedViews.FeedbackView({
|
||||
feedbackApiClient: new loop.FeedbackAPIClient({
|
||||
baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
|
||||
product: navigator.mozLoop.getLoopCharPref("feedback.product")
|
||||
})
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@ -267,7 +250,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
function init() {
|
||||
// Do the initial L10n setup, we do this before anything
|
||||
// else to ensure the L10n environment is setup correctly.
|
||||
mozL10n.initialize(window.navigator.mozLoop);
|
||||
mozL10n.initialize(navigator.mozLoop);
|
||||
|
||||
document.title = mozL10n.get("incoming_call_title");
|
||||
|
||||
@ -280,7 +263,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
|
||||
return {
|
||||
ConversationRouter: ConversationRouter,
|
||||
EndedCallView: EndedCallView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
};
|
||||
|
@ -108,12 +108,21 @@ loop.panel = (function(_, mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tosHTML = __("legal_text_and_links", {
|
||||
"terms_of_use_url": "https://accounts.firefox.com/legal/terms",
|
||||
"privacy_notice_url": "www.mozilla.org/privacy/"
|
||||
});
|
||||
|
||||
if (this.state.seenToS == "unseen") {
|
||||
var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
|
||||
var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
|
||||
var tosHTML = __("legal_text_and_links2", {
|
||||
"terms_of_use": React.renderComponentToStaticMarkup(
|
||||
React.DOM.a({href: terms_of_use_url, target: "_blank"},
|
||||
__("legal_text_tos")
|
||||
)
|
||||
),
|
||||
"privacy_notice": React.renderComponentToStaticMarkup(
|
||||
React.DOM.a({href: privacy_notice_url, target: "_blank"},
|
||||
__("legal_text_privacy")
|
||||
)
|
||||
),
|
||||
});
|
||||
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
||||
return React.DOM.p({className: "terms-service",
|
||||
dangerouslySetInnerHTML: {__html: tosHTML}});
|
||||
|
@ -108,12 +108,21 @@ loop.panel = (function(_, mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tosHTML = __("legal_text_and_links", {
|
||||
"terms_of_use_url": "https://accounts.firefox.com/legal/terms",
|
||||
"privacy_notice_url": "www.mozilla.org/privacy/"
|
||||
});
|
||||
|
||||
if (this.state.seenToS == "unseen") {
|
||||
var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
|
||||
var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
|
||||
var tosHTML = __("legal_text_and_links2", {
|
||||
"terms_of_use": React.renderComponentToStaticMarkup(
|
||||
<a href={terms_of_use_url} target="_blank">
|
||||
{__("legal_text_tos")}
|
||||
</a>
|
||||
),
|
||||
"privacy_notice": React.renderComponentToStaticMarkup(
|
||||
<a href={privacy_notice_url} target="_blank">
|
||||
{__("legal_text_privacy")}
|
||||
</a>
|
||||
),
|
||||
});
|
||||
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
||||
return <p className="terms-service"
|
||||
dangerouslySetInnerHTML={{__html: tosHTML}}></p>;
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
<div id="main"></div>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/libs/react-0.10.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/react-0.11.1.js"></script>
|
||||
<script type="text/javascript" src="loop/libs/l10n.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
|
@ -238,3 +238,105 @@
|
||||
color: #FFF;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
/* Expired call url page */
|
||||
|
||||
.expired-url-info {
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.promote-firefox {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.promote-firefox h3 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Feedback form */
|
||||
|
||||
.feedback {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.feedback h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.feedback .faces {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.feedback .face {
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0px 1px 2px #CCC;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin: 0px 10px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #fbfbfb;
|
||||
background-size: 60px auto;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.feedback .face:hover {
|
||||
border: 1px solid #DDD;
|
||||
background-color: #FEFEFE;
|
||||
}
|
||||
|
||||
.feedback .face.face-happy {
|
||||
background-image: url("../img/happy.png");
|
||||
}
|
||||
|
||||
.feedback .face.face-sad {
|
||||
background-image: url("../img/sad.png");
|
||||
}
|
||||
|
||||
.feedback button.back {
|
||||
border-radius: 2px;
|
||||
border: 1px solid #CCC;
|
||||
color: #CCC;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 3px 10px;
|
||||
display: inline;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.feedback label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feedback form input[type="radio"] {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.feedback form button[type="submit"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feedback form input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feedback .info {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #CCC;
|
||||
text-align: center;
|
||||
}
|
||||
|
BIN
browser/components/loop/content/shared/img/happy.png
Normal file
BIN
browser/components/loop/content/shared/img/happy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
browser/components/loop/content/shared/img/sad.png
Normal file
BIN
browser/components/loop/content/shared/img/sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -0,0 +1,92 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.FeedbackAPIClient = (function($) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Feedback API client. Sends feedback data to an input.mozilla.com compatible
|
||||
* API.
|
||||
*
|
||||
* Available settings:
|
||||
* - {String} baseUrl Base API url (required)
|
||||
* - {String} product Product name (required)
|
||||
*
|
||||
* @param {Object} settings Settings.
|
||||
* @link http://fjord.readthedocs.org/en/latest/api.html
|
||||
*/
|
||||
function FeedbackAPIClient(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.hasOwnProperty("baseUrl")) {
|
||||
throw new Error("Missing required baseUrl setting.");
|
||||
}
|
||||
this._baseUrl = settings.baseUrl;
|
||||
if (!settings.hasOwnProperty("product")) {
|
||||
throw new Error("Missing required product setting.");
|
||||
}
|
||||
this._product = settings.product;
|
||||
}
|
||||
|
||||
FeedbackAPIClient.prototype = {
|
||||
/**
|
||||
* Formats Feedback data to match the API spec.
|
||||
*
|
||||
* @param {Object} fields Feedback form data.
|
||||
* @return {Object} Formatted data.
|
||||
*/
|
||||
_formatData: function(fields) {
|
||||
var formatted = {};
|
||||
|
||||
if (typeof fields !== "object") {
|
||||
throw new Error("Invalid feedback data provided.");
|
||||
}
|
||||
|
||||
formatted.product = this._product;
|
||||
formatted.happy = fields.happy;
|
||||
formatted.category = fields.category;
|
||||
|
||||
// Default description field value
|
||||
if (!fields.description) {
|
||||
formatted.description = (fields.happy ? "Happy" : "Sad") + " User";
|
||||
} else {
|
||||
formatted.description = fields.description;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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._formatData(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;
|
||||
console.error(message, httpError, JSON.stringify(jqXHR.responseJSON));
|
||||
cb(new Error(message + ": " + httpError));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return FeedbackAPIClient;
|
||||
})(jQuery);
|
@ -166,7 +166,6 @@ loop.shared.router = (function(l10n) {
|
||||
* Session has ended. Notifies the user and ends the call.
|
||||
*/
|
||||
_onSessionEnded: function() {
|
||||
this._notifier.warnL10n("call_has_ended");
|
||||
this.endCall();
|
||||
},
|
||||
|
||||
|
@ -13,6 +13,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
|
||||
var sharedModels = loop.shared.models;
|
||||
var __ = l10n.get;
|
||||
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
||||
|
||||
/**
|
||||
* L10n view. Translates resulting view DOM fragment once rendered.
|
||||
@ -352,6 +353,239 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback outer layout.
|
||||
*
|
||||
* Props:
|
||||
* -
|
||||
*/
|
||||
var FeedbackLayout = React.createClass({displayName: 'FeedbackLayout',
|
||||
propTypes: {
|
||||
children: React.PropTypes.component.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var backButton = React.DOM.div(null);
|
||||
if (this.props.reset) {
|
||||
backButton = (
|
||||
React.DOM.button({className: "back", type: "button", onClick: this.props.reset},
|
||||
"« ", __("feedback_back_button")
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
React.DOM.div({className: "feedback"},
|
||||
backButton,
|
||||
React.DOM.h3(null, this.props.title),
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Detailed feedback form.
|
||||
*/
|
||||
var FeedbackForm = React.createClass({displayName: 'FeedbackForm',
|
||||
propTypes: {
|
||||
pending: React.PropTypes.bool,
|
||||
sendFeedback: React.PropTypes.func,
|
||||
reset: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {category: "", description: ""};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {pending: false};
|
||||
},
|
||||
|
||||
_getCategories: function() {
|
||||
return {
|
||||
audio_quality: __("feedback_category_audio_quality"),
|
||||
video_quality: __("feedback_category_video_quality"),
|
||||
disconnected : __("feedback_category_was_disconnected"),
|
||||
confusing: __("feedback_category_confusing"),
|
||||
other: __("feedback_category_other")
|
||||
};
|
||||
},
|
||||
|
||||
_getCategoryFields: function() {
|
||||
var categories = this._getCategories();
|
||||
return Object.keys(categories).map(function(category, key) {
|
||||
return (
|
||||
React.DOM.label({key: key},
|
||||
React.DOM.input({type: "radio", ref: "category", name: "category",
|
||||
value: category,
|
||||
onChange: this.handleCategoryChange}),
|
||||
categories[category]
|
||||
)
|
||||
);
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the form is ready for submission:
|
||||
* - a category (reason) must be chosen
|
||||
* - no feedback submission should be pending
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_isFormReady: function() {
|
||||
return this.state.category !== "" && !this.props.pending;
|
||||
},
|
||||
|
||||
handleCategoryChange: function(event) {
|
||||
var category = event.target.value;
|
||||
if (category !== "other") {
|
||||
// resets description text field
|
||||
this.setState({description: ""});
|
||||
}
|
||||
this.setState({category: category});
|
||||
},
|
||||
|
||||
handleCustomTextChange: function(event) {
|
||||
this.setState({description: event.target.value});
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
this.props.sendFeedback({
|
||||
happy: false,
|
||||
category: this.state.category,
|
||||
description: this.state.description
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
FeedbackLayout({title: __("feedback_what_makes_you_sad"),
|
||||
reset: this.props.reset},
|
||||
React.DOM.form({onSubmit: this.handleFormSubmit},
|
||||
this._getCategoryFields(),
|
||||
React.DOM.p(null, React.DOM.input({type: "text", ref: "description", name: "description",
|
||||
disabled: this.state.category !== "other",
|
||||
onChange: this.handleCustomTextChange,
|
||||
value: this.state.description})),
|
||||
React.DOM.button({type: "submit", className: "btn btn-success",
|
||||
disabled: !this._isFormReady()},
|
||||
__("feedback_submit_button")
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback received view.
|
||||
*/
|
||||
var FeedbackReceived = React.createClass({displayName: 'FeedbackReceived',
|
||||
getInitialState: function() {
|
||||
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._timer = setInterval(function() {
|
||||
this.setState({countdown: this.state.countdown - 1});
|
||||
}.bind(this), 1000);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.countdown < 1) {
|
||||
clearInterval(this._timer);
|
||||
window.close();
|
||||
}
|
||||
return (
|
||||
FeedbackLayout({title: __("feedback_thank_you_heading")},
|
||||
React.DOM.p({className: "info thank-you"}, __("feedback_window_will_close_in", {
|
||||
countdown: this.state.countdown
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback view.
|
||||
*/
|
||||
var FeedbackView = React.createClass({displayName: 'FeedbackView',
|
||||
propTypes: {
|
||||
// A loop.FeedbackAPIClient instance
|
||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
||||
// The current feedback submission flow step name
|
||||
step: React.PropTypes.oneOf(["start", "form", "finished"])
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {pending: false, step: this.props.step || "start"};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {step: "start"};
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
|
||||
handleHappyClick: function() {
|
||||
this.sendFeedback({happy: true}, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
handleSadClick: function() {
|
||||
this.setState({step: "form"});
|
||||
},
|
||||
|
||||
sendFeedback: function(fields) {
|
||||
// Setting state.pending to true will disable the submit button to avoid
|
||||
// multiple submissions
|
||||
this.setState({pending: true});
|
||||
// Sends feedback data
|
||||
this.props.feedbackApiClient.send(fields, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
_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.step) {
|
||||
case "finished":
|
||||
return FeedbackReceived(null);
|
||||
case "form":
|
||||
return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient,
|
||||
sendFeedback: this.sendFeedback,
|
||||
reset: this.reset,
|
||||
pending: this.state.pending});
|
||||
default:
|
||||
return (
|
||||
FeedbackLayout({title: __("feedback_call_experience_heading")},
|
||||
React.DOM.div({className: "faces"},
|
||||
React.DOM.button({className: "face face-happy",
|
||||
onClick: this.handleHappyClick}),
|
||||
React.DOM.button({className: "face face-sad",
|
||||
onClick: this.handleSadClick})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
@ -518,6 +752,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
BaseView: BaseView,
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
FeedbackView: FeedbackView,
|
||||
MediaControlButton: MediaControlButton,
|
||||
NotificationListView: NotificationListView,
|
||||
NotificationView: NotificationView,
|
||||
|
@ -13,6 +13,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
|
||||
var sharedModels = loop.shared.models;
|
||||
var __ = l10n.get;
|
||||
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
||||
|
||||
/**
|
||||
* L10n view. Translates resulting view DOM fragment once rendered.
|
||||
@ -352,6 +353,239 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback outer layout.
|
||||
*
|
||||
* Props:
|
||||
* -
|
||||
*/
|
||||
var FeedbackLayout = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.component.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var backButton = <div />;
|
||||
if (this.props.reset) {
|
||||
backButton = (
|
||||
<button className="back" type="button" onClick={this.props.reset}>
|
||||
« {__("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: {
|
||||
pending: React.PropTypes.bool,
|
||||
sendFeedback: React.PropTypes.func,
|
||||
reset: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {category: "", description: ""};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {pending: false};
|
||||
},
|
||||
|
||||
_getCategories: function() {
|
||||
return {
|
||||
audio_quality: __("feedback_category_audio_quality"),
|
||||
video_quality: __("feedback_category_video_quality"),
|
||||
disconnected : __("feedback_category_was_disconnected"),
|
||||
confusing: __("feedback_category_confusing"),
|
||||
other: __("feedback_category_other")
|
||||
};
|
||||
},
|
||||
|
||||
_getCategoryFields: function() {
|
||||
var categories = this._getCategories();
|
||||
return Object.keys(categories).map(function(category, key) {
|
||||
return (
|
||||
<label key={key}>
|
||||
<input type="radio" ref="category" name="category"
|
||||
value={category}
|
||||
onChange={this.handleCategoryChange} />
|
||||
{categories[category]}
|
||||
</label>
|
||||
);
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the form is ready for submission:
|
||||
* - a category (reason) must be chosen
|
||||
* - no feedback submission should be pending
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_isFormReady: function() {
|
||||
return this.state.category !== "" && !this.props.pending;
|
||||
},
|
||||
|
||||
handleCategoryChange: function(event) {
|
||||
var category = event.target.value;
|
||||
if (category !== "other") {
|
||||
// resets description text field
|
||||
this.setState({description: ""});
|
||||
}
|
||||
this.setState({category: category});
|
||||
},
|
||||
|
||||
handleCustomTextChange: function(event) {
|
||||
this.setState({description: event.target.value});
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
this.props.sendFeedback({
|
||||
happy: false,
|
||||
category: this.state.category,
|
||||
description: this.state.description
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<FeedbackLayout title={__("feedback_what_makes_you_sad")}
|
||||
reset={this.props.reset}>
|
||||
<form onSubmit={this.handleFormSubmit}>
|
||||
{this._getCategoryFields()}
|
||||
<p><input type="text" ref="description" name="description"
|
||||
disabled={this.state.category !== "other"}
|
||||
onChange={this.handleCustomTextChange}
|
||||
value={this.state.description} /></p>
|
||||
<button type="submit" className="btn btn-success"
|
||||
disabled={!this._isFormReady()}>
|
||||
{__("feedback_submit_button")}
|
||||
</button>
|
||||
</form>
|
||||
</FeedbackLayout>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback received view.
|
||||
*/
|
||||
var FeedbackReceived = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._timer = setInterval(function() {
|
||||
this.setState({countdown: this.state.countdown - 1});
|
||||
}.bind(this), 1000);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.countdown < 1) {
|
||||
clearInterval(this._timer);
|
||||
window.close();
|
||||
}
|
||||
return (
|
||||
<FeedbackLayout title={__("feedback_thank_you_heading")}>
|
||||
<p className="info thank-you">{__("feedback_window_will_close_in", {
|
||||
countdown: this.state.countdown
|
||||
})}</p>
|
||||
</FeedbackLayout>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback view.
|
||||
*/
|
||||
var FeedbackView = React.createClass({
|
||||
propTypes: {
|
||||
// A loop.FeedbackAPIClient instance
|
||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
||||
// The current feedback submission flow step name
|
||||
step: React.PropTypes.oneOf(["start", "form", "finished"])
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {pending: false, step: this.props.step || "start"};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {step: "start"};
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
|
||||
handleHappyClick: function() {
|
||||
this.sendFeedback({happy: true}, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
handleSadClick: function() {
|
||||
this.setState({step: "form"});
|
||||
},
|
||||
|
||||
sendFeedback: function(fields) {
|
||||
// Setting state.pending to true will disable the submit button to avoid
|
||||
// multiple submissions
|
||||
this.setState({pending: true});
|
||||
// Sends feedback data
|
||||
this.props.feedbackApiClient.send(fields, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
_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.step) {
|
||||
case "finished":
|
||||
return <FeedbackReceived />;
|
||||
case "form":
|
||||
return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
|
||||
sendFeedback={this.sendFeedback}
|
||||
reset={this.reset}
|
||||
pending={this.state.pending} />;
|
||||
default:
|
||||
return (
|
||||
<FeedbackLayout title={__("feedback_call_experience_heading")}>
|
||||
<div className="faces">
|
||||
<button className="face face-happy"
|
||||
onClick={this.handleHappyClick}></button>
|
||||
<button className="face face-sad"
|
||||
onClick={this.handleSadClick}></button>
|
||||
</div>
|
||||
</FeedbackLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
@ -518,6 +752,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
BaseView: BaseView,
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
FeedbackView: FeedbackView,
|
||||
MediaControlButton: MediaControlButton,
|
||||
NotificationListView: NotificationListView,
|
||||
NotificationView: NotificationView,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,8 @@ 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/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/loading-icon.gif (content/shared/img/loading-icon.gif)
|
||||
@ -39,13 +41,14 @@ browser.jar:
|
||||
content/browser/loop/shared/img/dropdown-inverse@2x.png (content/shared/img/dropdown-inverse@2x.png)
|
||||
|
||||
# Shared scripts
|
||||
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
|
||||
content/browser/loop/shared/js/router.js (content/shared/js/router.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/js/utils.js (content/shared/js/utils.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/router.js (content/shared/js/router.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
|
||||
|
||||
# Shared libs
|
||||
content/browser/loop/shared/libs/react-0.10.0.js (content/shared/libs/react-0.10.0.js)
|
||||
content/browser/loop/shared/libs/react-0.11.1.js (content/shared/libs/react-0.11.1.js)
|
||||
content/browser/loop/shared/libs/lodash-2.4.1.js (content/shared/libs/lodash-2.4.1.js)
|
||||
content/browser/loop/shared/libs/jquery-2.1.0.js (content/shared/libs/jquery-2.1.0.js)
|
||||
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
|
||||
|
@ -26,7 +26,7 @@
|
||||
</script>
|
||||
<script type="text/javascript" src="shared/libs/sdk.js"></script>
|
||||
<script type="text/javascript" src="libs/webl10n-20130617.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/react-0.10.0.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/react-0.11.1.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
|
||||
|
@ -47,7 +47,7 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete window.navigator.mozLoop;
|
||||
delete navigator.mozLoop;
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
@ -79,7 +79,7 @@ describe("loop.conversation", function() {
|
||||
|
||||
sinon.assert.calledOnce(document.mozL10n.initialize);
|
||||
sinon.assert.calledWithExactly(document.mozL10n.initialize,
|
||||
window.navigator.mozLoop);
|
||||
navigator.mozLoop);
|
||||
});
|
||||
|
||||
it("should set the document title", function() {
|
||||
@ -159,16 +159,16 @@ describe("loop.conversation", function() {
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return TestUtils.isComponentOfType(value,
|
||||
return TestUtils.isDescriptorOfType(value,
|
||||
loop.conversation.IncomingCallView);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should start alerting", function() {
|
||||
sandbox.stub(window.navigator.mozLoop, "startAlerting");
|
||||
sandbox.stub(navigator.mozLoop, "startAlerting");
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
sinon.assert.calledOnce(window.navigator.mozLoop.startAlerting);
|
||||
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
|
||||
});
|
||||
});
|
||||
|
||||
@ -187,10 +187,10 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
|
||||
it("should stop alerting", function() {
|
||||
sandbox.stub(window.navigator.mozLoop, "stopAlerting");
|
||||
sandbox.stub(navigator.mozLoop, "stopAlerting");
|
||||
router.accept();
|
||||
|
||||
sinon.assert.calledOnce(window.navigator.mozLoop.stopAlerting);
|
||||
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
|
||||
});
|
||||
});
|
||||
|
||||
@ -207,7 +207,7 @@ describe("loop.conversation", function() {
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return TestUtils.isComponentOfType(value,
|
||||
return TestUtils.isDescriptorOfType(value,
|
||||
loop.shared.views.ConversationView);
|
||||
}));
|
||||
});
|
||||
@ -241,25 +241,56 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
|
||||
it("should stop alerting", function() {
|
||||
sandbox.stub(window.navigator.mozLoop, "stopAlerting");
|
||||
sandbox.stub(navigator.mozLoop, "stopAlerting");
|
||||
router.decline();
|
||||
|
||||
sinon.assert.calledOnce(window.navigator.mozLoop.stopAlerting);
|
||||
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#ended", function() {
|
||||
describe("#feedback", function() {
|
||||
var oldTitle;
|
||||
|
||||
beforeEach(function() {
|
||||
oldTitle = document.title;
|
||||
sandbox.stub(document.mozL10n, "get").returns("Call ended");
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(loop, "FeedbackAPIClient");
|
||||
sandbox.stub(router, "loadReactComponent");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
document.title = oldTitle;
|
||||
});
|
||||
|
||||
// XXX When the call is ended gracefully, we should check that we
|
||||
// close connections nicely
|
||||
it("should close the window");
|
||||
// close connections nicely (see bug 1046744)
|
||||
it("should display a feedback form view", function() {
|
||||
router.feedback();
|
||||
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return TestUtils.isDescriptorOfType(value,
|
||||
loop.shared.views.FeedbackView);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should update the conversation window title", function() {
|
||||
router.feedback();
|
||||
|
||||
expect(document.title).eql("Call ended");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#blocked", function() {
|
||||
it("should call mozLoop.stopAlerting", function() {
|
||||
sandbox.stub(window.navigator.mozLoop, "stopAlerting");
|
||||
sandbox.stub(navigator.mozLoop, "stopAlerting");
|
||||
router.declineAndBlock();
|
||||
|
||||
sinon.assert.calledOnce(window.navigator.mozLoop.stopAlerting);
|
||||
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
|
||||
});
|
||||
|
||||
it("should call delete call", function() {
|
||||
@ -319,44 +350,31 @@ describe("loop.conversation", function() {
|
||||
sinon.assert.calledWith(router.navigate, "call/ongoing");
|
||||
});
|
||||
|
||||
it("should navigate to call/ended when the call session ends",
|
||||
it("should navigate to call/feedback when the call session ends",
|
||||
function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
sinon.assert.calledWith(router.navigate, "call/feedback");
|
||||
});
|
||||
|
||||
it("should navigate to call/ended when peer hangs up", function() {
|
||||
it("should navigate to call/feedback when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
sinon.assert.calledWith(router.navigate, "call/feedback");
|
||||
});
|
||||
|
||||
it("should navigate to call/{token} when network disconnects",
|
||||
it("should navigate to call/feedback when network disconnects",
|
||||
function() {
|
||||
conversation.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
sinon.assert.calledWith(router.navigate, "call/feedback");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EndedCallView", function() {
|
||||
describe("#closeWindow", function() {
|
||||
it("should close the conversation window", function() {
|
||||
sandbox.stub(window, "close");
|
||||
var view = new loop.conversation.EndedCallView();
|
||||
|
||||
view.closeWindow({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(window.close);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IncomingCallView", function() {
|
||||
var view, model;
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
<div id="fixtures"></div>
|
||||
<!-- libs -->
|
||||
<script src="../../content/libs/l10n.js"></script>
|
||||
<script src="../../content/shared/libs/react-0.10.0.js"></script>
|
||||
<script src="../../content/shared/libs/react-0.11.1.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
@ -33,6 +33,7 @@
|
||||
|
||||
<!-- 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/router.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
|
@ -110,7 +110,7 @@ describe("loop.panel", function() {
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWithExactly(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isComponentOfType(
|
||||
return React.addons.TestUtils.isDescriptorOfType(
|
||||
value, loop.panel.PanelView);
|
||||
}));
|
||||
});
|
||||
|
139
browser/components/loop/test/shared/feedbackApiClient_test.js
Normal file
139
browser/components/loop/test/shared/feedbackApiClient_test.js
Normal file
@ -0,0 +1,139 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.FeedbackAPIClient", function() {
|
||||
"use strict";
|
||||
|
||||
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({baseUrl: "http://fake"});
|
||||
}).to.Throw(/required product/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.FeedbackAPIClient({
|
||||
baseUrl: "http://fake/feedback",
|
||||
product: "Hello"
|
||||
});
|
||||
});
|
||||
|
||||
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 throw on invalid feedback data", function() {
|
||||
expect(function() {
|
||||
client.send("invalid data", function(){});
|
||||
}).to.Throw(/Invalid/);
|
||||
});
|
||||
|
||||
it("should call passed callback on success", function() {
|
||||
var cb = sandbox.spy();
|
||||
var fakeResponseData = {description: "confusing"};
|
||||
client.send({reason: "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({reason: "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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -16,7 +16,7 @@
|
||||
<div id="fixtures"></div>
|
||||
|
||||
<!-- libs -->
|
||||
<script src="../../content/shared/libs/react-0.10.0.js"></script>
|
||||
<script src="../../content/shared/libs/react-0.11.1.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
@ -36,11 +36,13 @@
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="models_test.js"></script>
|
||||
<script src="views_test.js"></script>
|
||||
<script src="router_test.js"></script>
|
||||
<script src="feedbackApiClient_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
$("#mocha").append("<p id='complete'>Complete.</p>");
|
||||
|
@ -161,14 +161,6 @@ describe("loop.shared.router", function() {
|
||||
sinon.assert.calledOnce(router.endCall);
|
||||
});
|
||||
|
||||
it("should warn the user that the session has ended", function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(notifier.warnL10n);
|
||||
sinon.assert.calledWithExactly(notifier.warnL10n,
|
||||
"call_has_ended");
|
||||
});
|
||||
|
||||
it("should warn the user when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
|
@ -401,6 +401,144 @@ describe("loop.shared.views", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("FeedbackView", function() {
|
||||
var comp, fakeFeedbackApiClient;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeFeedbackApiClient = {send: sandbox.stub()};
|
||||
comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
|
||||
feedbackApiClient: fakeFeedbackApiClient
|
||||
}));
|
||||
});
|
||||
|
||||
// local test helpers
|
||||
function clickHappyFace(comp) {
|
||||
var happyFace = comp.getDOMNode().querySelector(".face-happy");
|
||||
TestUtils.Simulate.click(happyFace);
|
||||
}
|
||||
|
||||
function clickSadFace(comp) {
|
||||
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
||||
TestUtils.Simulate.click(sadFace);
|
||||
}
|
||||
|
||||
function fillSadFeedbackForm(comp, category, text) {
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='" + category + "']"));
|
||||
|
||||
if (text) {
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[name='description']"), {
|
||||
target: {value: "fake reason"}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function submitSadFeedbackForm(comp, category, text) {
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
}
|
||||
|
||||
describe("Happy feedback", function() {
|
||||
it("should send feedback data when clicking on the happy face",
|
||||
function() {
|
||||
clickHappyFace(comp);
|
||||
|
||||
sinon.assert.calledOnce(fakeFeedbackApiClient.send);
|
||||
sinon.assert.calledWith(fakeFeedbackApiClient.send, {happy: true});
|
||||
});
|
||||
|
||||
it("should thank the user once happy feedback data is sent", function() {
|
||||
fakeFeedbackApiClient.send = function(data, cb) {
|
||||
cb();
|
||||
};
|
||||
|
||||
clickHappyFace(comp);
|
||||
|
||||
expect(comp.getDOMNode()
|
||||
.querySelectorAll(".feedback .thank-you").length).eql(1);
|
||||
expect(comp.getDOMNode().querySelector("button.back")).to.be.a("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").length).eql(1);
|
||||
});
|
||||
|
||||
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 enable the form submit button once a choice is made",
|
||||
function() {
|
||||
clickSadFace(comp);
|
||||
|
||||
fillSadFeedbackForm(comp, "confusing");
|
||||
|
||||
expect(comp.getDOMNode()
|
||||
.querySelector("form button").disabled).eql(false);
|
||||
});
|
||||
|
||||
it("should disable the form submit button once the form is submitted",
|
||||
function() {
|
||||
clickSadFace(comp);
|
||||
fillSadFeedbackForm(comp, "confusing");
|
||||
|
||||
submitSadFeedbackForm(comp);
|
||||
|
||||
expect(comp.getDOMNode()
|
||||
.querySelector("form button").disabled).eql(true);
|
||||
});
|
||||
|
||||
it("should send feedback data when the form is submitted", function() {
|
||||
clickSadFace(comp);
|
||||
fillSadFeedbackForm(comp, "confusing");
|
||||
|
||||
submitSadFeedbackForm(comp);
|
||||
|
||||
sinon.assert.calledOnce(fakeFeedbackApiClient.send);
|
||||
sinon.assert.calledWithMatch(fakeFeedbackApiClient.send, {
|
||||
happy: false,
|
||||
category: "confusing"
|
||||
});
|
||||
});
|
||||
|
||||
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(fakeFeedbackApiClient.send);
|
||||
sinon.assert.calledWith(fakeFeedbackApiClient.send, {
|
||||
happy: false,
|
||||
category: "other",
|
||||
description: "fake reason"
|
||||
});
|
||||
});
|
||||
|
||||
it("should thank the user when feedback data has been sent", function() {
|
||||
fakeFeedbackApiClient.send = function(data, cb) {
|
||||
cb();
|
||||
};
|
||||
clickSadFace(comp);
|
||||
fillSadFeedbackForm(comp, "confusing");
|
||||
submitSadFeedbackForm(comp);
|
||||
|
||||
expect(comp.getDOMNode()
|
||||
.querySelectorAll(".feedback .thank-you").length).eql(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationView", function() {
|
||||
var collection, model, view;
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
<div id="messages"></div>
|
||||
<div id="fixtures"></div>
|
||||
<!-- libs -->
|
||||
<script src="../../content/shared/libs/react-0.10.0.js"></script>
|
||||
<script src="../../content/shared/libs/react-0.11.1.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
|
@ -149,7 +149,7 @@ describe("loop.webapp", function() {
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isComponentOfType(
|
||||
return React.addons.TestUtils.isDescriptorOfType(
|
||||
value, loop.webapp.CallUrlExpiredView);
|
||||
}));
|
||||
});
|
||||
@ -168,7 +168,7 @@ describe("loop.webapp", function() {
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWithExactly(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isComponentOfType(
|
||||
return React.addons.TestUtils.isDescriptorOfType(
|
||||
value, loop.webapp.ConversationFormView);
|
||||
}));
|
||||
});
|
||||
@ -193,7 +193,7 @@ describe("loop.webapp", function() {
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isComponentOfType(
|
||||
return React.addons.TestUtils.isDescriptorOfType(
|
||||
value, loop.shared.views.ConversationView);
|
||||
}));
|
||||
});
|
||||
|
@ -20,6 +20,8 @@
|
||||
<script src="../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script src="../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../content/shared/js/utils.js"></script>
|
||||
<script src="../content/shared/js/models.js"></script>
|
||||
<script src="../content/shared/js/router.js"></script>
|
||||
<script src="../content/shared/js/views.js"></script>
|
||||
|
@ -45,3 +45,10 @@
|
||||
.showcase > section .example > h3 {
|
||||
border-bottom: 1px dashed #aaa;
|
||||
}
|
||||
|
||||
.showcase p.note {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
// 3. Shared components
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var ConversationView = loop.shared.views.ConversationView;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
|
||||
// Local helpers
|
||||
function returnTrue() {
|
||||
@ -32,6 +33,13 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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({
|
||||
baseUrl: "https://input.allizom.org/api/v1/feedback",
|
||||
product: "Loop"
|
||||
});
|
||||
|
||||
var Example = React.createClass({displayName: 'Example',
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
@ -114,6 +122,22 @@
|
||||
)
|
||||
),
|
||||
|
||||
Section({name: "FeedbackView"},
|
||||
React.DOM.p({className: "note"},
|
||||
React.DOM.strong(null, "Note:"), " For the useable demo, you can access submitted data at ",
|
||||
React.DOM.a({href: "https://input.allizom.org/"}, "input.allizom.org"), "."
|
||||
),
|
||||
Example({summary: "Default (useable demo)", dashed: "true", style: {width: "280px"}},
|
||||
FeedbackView({feedbackApiClient: stageFeedbackApiClient})
|
||||
),
|
||||
Example({summary: "Detailed form", dashed: "true", style: {width: "280px"}},
|
||||
FeedbackView({step: "form"})
|
||||
),
|
||||
Example({summary: "Thank you!", dashed: "true", style: {width: "280px"}},
|
||||
FeedbackView({step: "finished"})
|
||||
)
|
||||
),
|
||||
|
||||
Section({name: "CallUrlExpiredView"},
|
||||
Example({summary: "Firefox User"},
|
||||
CallUrlExpiredView({helper: {isFirefox: returnTrue}})
|
||||
|
@ -22,6 +22,7 @@
|
||||
// 3. Shared components
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var ConversationView = loop.shared.views.ConversationView;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
|
||||
// Local helpers
|
||||
function returnTrue() {
|
||||
@ -32,6 +33,13 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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({
|
||||
baseUrl: "https://input.allizom.org/api/v1/feedback",
|
||||
product: "Loop"
|
||||
});
|
||||
|
||||
var Example = React.createClass({
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
@ -114,6 +122,22 @@
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="FeedbackView">
|
||||
<p className="note">
|
||||
<strong>Note:</strong> For the useable demo, you can access submitted data at
|
||||
<a href="https://input.allizom.org/">input.allizom.org</a>.
|
||||
</p>
|
||||
<Example summary="Default (useable demo)" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView feedbackApiClient={stageFeedbackApiClient} />
|
||||
</Example>
|
||||
<Example summary="Detailed form" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView step="form" />
|
||||
</Example>
|
||||
<Example summary="Thank you!" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView step="finished" />
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="CallUrlExpiredView">
|
||||
<Example summary="Firefox User">
|
||||
<CallUrlExpiredView helper={{isFirefox: returnTrue}} />
|
||||
|
@ -53,6 +53,14 @@
|
||||
<popupset>
|
||||
<menupopup id="context-menu-popup">
|
||||
</menupopup>
|
||||
<menupopup id="texteditor-context-popup">
|
||||
<menuitem id="cMenu_cut"/>
|
||||
<menuitem id="cMenu_copy"/>
|
||||
<menuitem id="cMenu_paste"/>
|
||||
<menuitem id="cMenu_delete"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="cMenu_selectAll"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
<deck id="main-deck" flex="1">
|
||||
|
@ -51,6 +51,7 @@ var ItchEditor = Class({
|
||||
* ItchEditor.prototype.initialize.apply(this, arguments)
|
||||
*/
|
||||
initialize: function(host) {
|
||||
this.host = host;
|
||||
this.doc = host.document;
|
||||
this.label = "";
|
||||
this.elt = this.doc.createElement("vbox");
|
||||
@ -165,7 +166,8 @@ var TextEditor = Class({
|
||||
lineNumbers: true,
|
||||
extraKeys: this.extraKeys,
|
||||
themeSwitching: false,
|
||||
autocomplete: true
|
||||
autocomplete: true,
|
||||
contextMenu: this.host.textEditorContextMenuPopup
|
||||
});
|
||||
|
||||
// Trigger a few editor specific events on `this`.
|
||||
|
@ -175,6 +175,12 @@ var ProjectEditor = Class({
|
||||
|
||||
_buildMenubar: function() {
|
||||
|
||||
this.contextMenuPopup = this.document.getElementById("context-menu-popup");
|
||||
this.contextMenuPopup.addEventListener("popupshowing", this._updateContextMenuItems);
|
||||
|
||||
this.textEditorContextMenuPopup = this.document.getElementById("texteditor-context-popup");
|
||||
this.textEditorContextMenuPopup.addEventListener("popupshowing", this._updateMenuItems);
|
||||
|
||||
this.editMenu = this.document.getElementById("edit-menu");
|
||||
this.fileMenu = this.document.getElementById("file-menu");
|
||||
|
||||
@ -191,6 +197,7 @@ var ProjectEditor = Class({
|
||||
body.appendChild(this.editorCommandset);
|
||||
body.appendChild(this.editorKeyset);
|
||||
body.appendChild(this.contextMenuPopup);
|
||||
body.appendChild(this.textEditorContextMenuPopup);
|
||||
|
||||
let index = this.menuindex || 0;
|
||||
this.menubar.insertBefore(this.editMenu, this.menubar.children[index]);
|
||||
@ -232,9 +239,6 @@ var ProjectEditor = Class({
|
||||
this.editorCommandset = this.document.getElementById("editMenuCommands");
|
||||
this.editorKeyset = this.document.getElementById("editMenuKeys");
|
||||
|
||||
this.contextMenuPopup = this.document.getElementById("context-menu-popup");
|
||||
this.contextMenuPopup.addEventListener("popupshowing", this._updateContextMenuItems);
|
||||
|
||||
this.projectEditorCommandset.addEventListener("command", (evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
@ -313,6 +317,7 @@ var ProjectEditor = Class({
|
||||
this.editorCommandset.remove();
|
||||
this.editorKeyset.remove();
|
||||
this.contextMenuPopup.remove();
|
||||
this.textEditorContextMenuPopup.remove();
|
||||
this.editMenu.remove();
|
||||
this.fileMenu.remove();
|
||||
|
||||
|
@ -7,6 +7,8 @@ support-files =
|
||||
|
||||
[browser_projecteditor_app_options.js]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[browser_projecteditor_contextmenu_01.js]
|
||||
[browser_projecteditor_contextmenu_02.js]
|
||||
[browser_projecteditor_delete_file.js]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[browser_projecteditor_editing_01.js]
|
||||
|
@ -0,0 +1,27 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that context menus append to the correct document.
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
let projecteditor = yield addProjectEditorTabForTempDirectory({
|
||||
menubar: false
|
||||
});
|
||||
ok(projecteditor, "ProjectEditor has loaded");
|
||||
|
||||
let contextMenuPopup = projecteditor.document.querySelector("#context-menu-popup");
|
||||
let textEditorContextMenuPopup = projecteditor.document.querySelector("#texteditor-context-popup");
|
||||
ok (contextMenuPopup, "The menu has loaded in the projecteditor document");
|
||||
ok (textEditorContextMenuPopup, "The menu has loaded in the projecteditor document");
|
||||
|
||||
let projecteditor2 = yield addProjectEditorTabForTempDirectory();
|
||||
let contextMenuPopup = projecteditor2.document.getElementById("context-menu-popup");
|
||||
let textEditorContextMenuPopup = projecteditor2.document.getElementById("texteditor-context-popup");
|
||||
ok (!contextMenuPopup, "The menu has NOT loaded in the projecteditor document");
|
||||
ok (!textEditorContextMenuPopup, "The menu has NOT loaded in the projecteditor document");
|
||||
ok (content.document.querySelector("#context-menu-popup"), "The menu has loaded in the specified element");
|
||||
ok (content.document.querySelector("#texteditor-context-popup"), "The menu has loaded in the specified element");
|
||||
});
|
@ -0,0 +1,62 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
loadHelperScript("helper_edits.js");
|
||||
|
||||
// Test context menu enabled / disabled state in editor
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
let projecteditor = yield addProjectEditorTabForTempDirectory();
|
||||
ok (projecteditor, "ProjectEditor has loaded");
|
||||
|
||||
let {textEditorContextMenuPopup} = projecteditor;
|
||||
|
||||
let cmdDelete = textEditorContextMenuPopup.querySelector("[command=cmd_delete]");
|
||||
let cmdSelectAll = textEditorContextMenuPopup.querySelector("[command=cmd_selectAll]");
|
||||
let cmdCut = textEditorContextMenuPopup.querySelector("[command=cmd_cut]");
|
||||
let cmdCopy = textEditorContextMenuPopup.querySelector("[command=cmd_copy]");
|
||||
let cmdPaste = textEditorContextMenuPopup.querySelector("[command=cmd_paste]");
|
||||
|
||||
info ("Opening resource");
|
||||
let resource = projecteditor.project.allResources()[2];
|
||||
yield selectFile(projecteditor, resource);
|
||||
let editor = projecteditor.currentEditor;
|
||||
editor.editor.focus();
|
||||
|
||||
info ("Opening context menu on resource");
|
||||
yield openContextMenuForEditor(editor, textEditorContextMenuPopup);
|
||||
|
||||
is (cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
|
||||
is (cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
|
||||
is (cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
|
||||
is (cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
|
||||
is (cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
|
||||
|
||||
info ("Setting a selection and repening context menu on resource");
|
||||
yield closeContextMenuForEditor(editor, textEditorContextMenuPopup);
|
||||
editor.editor.setSelection({line: 0, ch: 0}, {line: 0, ch: 2});
|
||||
yield openContextMenuForEditor(editor, textEditorContextMenuPopup);
|
||||
|
||||
is (cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled");
|
||||
is (cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
|
||||
is (cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
|
||||
is (cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
|
||||
is (cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled");
|
||||
});
|
||||
|
||||
function openContextMenuForEditor(editor, contextMenu) {
|
||||
let editorDoc = editor.editor.container.contentDocument;
|
||||
let shown = onPopupShow(contextMenu);
|
||||
EventUtils.synthesizeMouse(editorDoc.body, 2, 2,
|
||||
{type: "contextmenu", button: 2}, editorDoc.defaultView);
|
||||
yield shown;
|
||||
}
|
||||
function closeContextMenuForEditor(editor, contextMenu) {
|
||||
let editorDoc = editor.editor.container.contentDocument;
|
||||
let hidden = onPopupHidden(contextMenu);
|
||||
contextMenu.hidePopup();
|
||||
yield hidden;
|
||||
}
|
@ -82,7 +82,7 @@ let test = asyncTest(function*() {
|
||||
let editor = projecteditor.currentEditor;
|
||||
|
||||
editor.editor.focus();
|
||||
EventUtils.synthesizeKey("foo", { }, projecteditor.window);
|
||||
EventUtils.synthesizeKey("f", { }, projecteditor.window);
|
||||
|
||||
yield openAndCloseMenu(fileMenu);
|
||||
yield openAndCloseMenu(editMenu);
|
||||
@ -99,10 +99,10 @@ let test = asyncTest(function*() {
|
||||
});
|
||||
|
||||
function openAndCloseMenu(menu) {
|
||||
let shown = onPopupShow(menu)
|
||||
let shown = onPopupShow(menu);
|
||||
EventUtils.synthesizeMouseAtCenter(menu, {}, menu.ownerDocument.defaultView);
|
||||
yield shown;
|
||||
let hidden = onPopupHidden(menu)
|
||||
let hidden = onPopupHidden(menu);
|
||||
EventUtils.synthesizeMouseAtCenter(menu, {}, menu.ownerDocument.defaultView);
|
||||
yield hidden;
|
||||
}
|
||||
|
@ -131,8 +131,8 @@ Editor.modes = {
|
||||
* properties go to CodeMirror's documentation (see below).
|
||||
*
|
||||
* Other than that, it accepts one additional and optional
|
||||
* property contextMenu. This property should be an ID of
|
||||
* an element we can use as a context menu.
|
||||
* property contextMenu. This property should be an element, or
|
||||
* an ID of an element that we can use as a context menu.
|
||||
*
|
||||
* This object is also an event emitter.
|
||||
*
|
||||
@ -286,7 +286,9 @@ Editor.prototype = {
|
||||
cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault();
|
||||
if (!this.config.contextMenu) return;
|
||||
let popup = el.ownerDocument.getElementById(this.config.contextMenu);
|
||||
let popup = this.config.contextMenu;
|
||||
if (typeof popup == "string")
|
||||
popup = el.ownerDocument.getElementById(this.config.contextMenu);
|
||||
popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
|
||||
}, false);
|
||||
|
||||
|
@ -67,3 +67,4 @@ skip-if = os == "linux" || "mac" # bug 949355
|
||||
[browser_styleeditor_selectstylesheet.js]
|
||||
[browser_styleeditor_sourcemaps.js]
|
||||
[browser_styleeditor_sourcemap_watching.js]
|
||||
[browser_styleeditor_transition_rule.js]
|
||||
|
@ -0,0 +1,48 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
const NEW_RULE = "body { background-color: purple; }";
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
let {UI} = yield addTabAndOpenStyleEditors(2, null, TESTCASE_URI);
|
||||
|
||||
is(UI.editors.length, 2, "correct number of editors");
|
||||
|
||||
let editor = UI.editors[0];
|
||||
yield openEditor(editor);
|
||||
|
||||
// Set text twice in a row
|
||||
let styleChanges = listenForStyleChange(editor.styleSheet);
|
||||
|
||||
editor.sourceEditor.setText(NEW_RULE);
|
||||
editor.sourceEditor.setText(NEW_RULE + " ");
|
||||
|
||||
yield styleChanges;
|
||||
|
||||
let sheet = content.document.styleSheets[0];
|
||||
|
||||
// Test that we removed the transition rule, but kept the rule we added
|
||||
is(sheet.cssRules.length, 1, "only one rule in stylesheet");
|
||||
is(sheet.cssRules[0].cssText, NEW_RULE,
|
||||
"stylesheet only contains rule we added");
|
||||
});
|
||||
|
||||
/* Helpers */
|
||||
|
||||
function openEditor(editor) {
|
||||
let link = editor.summary.querySelector(".stylesheet-name");
|
||||
link.click();
|
||||
|
||||
return editor.getSourceEditor();
|
||||
}
|
||||
|
||||
function listenForStyleChange(sheet) {
|
||||
let deferred = promise.defer();
|
||||
sheet.on("style-applied", deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
@ -28,14 +28,29 @@ unmute_local_video_button_title=Unmute your video
|
||||
|
||||
peer_ended_conversation=Your peer ended the conversation.
|
||||
call_has_ended=Your call has ended.
|
||||
close_window=Close this window
|
||||
|
||||
cannot_start_call_session_not_ready=Can't start call, session is not ready.
|
||||
network_disconnected=The network connection terminated abruptly.
|
||||
|
||||
connection_error_see_console_notification=Call failed; see console for details.
|
||||
## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the
|
||||
## part between {{..}}
|
||||
legal_text_and_links=By using this product you agree to the <a \
|
||||
target="_blank" href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
||||
href="{{privacy_notice_url}}">Privacy Notice</a>.
|
||||
## LOCALIZATION NOTE (legal_text_and_links2): In this item, don't translate the
|
||||
## parts between {{..}} because these will be replaced with links with the labels
|
||||
## from legal_text_tos and legal_text_privacy.
|
||||
legal_text_and_links2=By using this product you agree to the {{terms_of_use}} \
|
||||
and {{privacy_notice}}.
|
||||
legal_text_tos = Terms of Use
|
||||
legal_text_privacy = Privacy Notice
|
||||
|
||||
feedback_call_experience_heading=How was your call experience?
|
||||
feedback_what_makes_you_sad=What makes you sad?
|
||||
feedback_thank_you_heading=Thank you for your feedback!
|
||||
feedback_category_audio_quality=Audio quality
|
||||
feedback_category_video_quality=Video quality
|
||||
feedback_category_was_disconnected=Was disconnected
|
||||
feedback_category_confusing=Confusing
|
||||
feedback_category_other=Other:
|
||||
feedback_submit_button=Submit
|
||||
feedback_back_button=Back
|
||||
## LOCALIZATION NOTE (feedback_window_will_close_in): In this item, don't
|
||||
## translate the part between {{..}}
|
||||
feedback_window_will_close_in=This window will close in {{countdown}} seconds
|
||||
|
@ -981,21 +981,24 @@ class Mochitest(MochitestUtilsMixin):
|
||||
vmwareHelper = None
|
||||
DEFAULT_TIMEOUT = 60.0
|
||||
mediaDevices = None
|
||||
structured_logger = None
|
||||
|
||||
# XXX use automation.py for test name to avoid breaking legacy
|
||||
# TODO: replace this with 'runtests.py' or 'mochitest' or the like
|
||||
test_name = 'automation.py'
|
||||
|
||||
def __init__(self):
|
||||
# Structured logger
|
||||
if self.structured_logger is None:
|
||||
self.structured_logger = StructuredLogger('mochitest')
|
||||
stream_handler = StreamHandler(stream=sys.stdout, formatter=MochitestFormatter())
|
||||
self.structured_logger.add_handler(stream_handler)
|
||||
Mochitest.structured_logger = self.structured_logger
|
||||
|
||||
super(Mochitest, self).__init__()
|
||||
|
||||
# Structured logger
|
||||
structured_log = StructuredLogger('mochitest')
|
||||
stream_handler = StreamHandler(stream=sys.stdout, formatter=MochitestFormatter())
|
||||
structured_log.add_handler(stream_handler)
|
||||
|
||||
# Structured logs parser
|
||||
self.message_logger = MessageLogger(logger=structured_log)
|
||||
self.message_logger = MessageLogger(logger=self.structured_logger)
|
||||
|
||||
# environment function for browserEnv
|
||||
self.environment = environment
|
||||
|
@ -18,7 +18,9 @@ import argparse
|
||||
class DMCli(object):
|
||||
|
||||
def __init__(self):
|
||||
self.commands = { 'install': { 'function': self.install,
|
||||
self.commands = { 'deviceroot': { 'function': self.deviceroot,
|
||||
'help': 'get device root directory for storing temporary files' },
|
||||
'install': { 'function': self.install,
|
||||
'args': [ { 'name': 'file' } ],
|
||||
'help': 'push this package file to the device and install it' },
|
||||
'uninstall': { 'function': self.uninstall,
|
||||
@ -219,6 +221,9 @@ class DMCli(object):
|
||||
else:
|
||||
self.parser.error("Unknown device manager type: %s" % type)
|
||||
|
||||
def deviceroot(self, args):
|
||||
return self.dm.deviceRoot
|
||||
|
||||
def push(self, args):
|
||||
(src, dest) = (args.local_file, args.remote_file)
|
||||
if os.path.isdir(src):
|
||||
|
@ -2070,6 +2070,10 @@ Requisition.prototype.exec = function(options) {
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null && typeof data === 'string') {
|
||||
data = data.replace(/^Protocol error: /, ''); // Temp fix for bug 1035296
|
||||
}
|
||||
|
||||
data = (data != null && data.isTypedData) ? data : {
|
||||
isTypedData: true,
|
||||
data: data,
|
||||
|
@ -24,8 +24,9 @@ loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-l
|
||||
let TRANSITION_CLASS = "moz-styleeditor-transitioning";
|
||||
let TRANSITION_DURATION_MS = 500;
|
||||
let TRANSITION_BUFFER_MS = 1000;
|
||||
let TRANSITION_RULE = "\
|
||||
:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\
|
||||
let TRANSITION_RULE_SELECTOR =
|
||||
".moz-styleeditor-transitioning:root, .moz-styleeditor-transitioning:root *";
|
||||
let TRANSITION_RULE = TRANSITION_RULE_SELECTOR + " {\
|
||||
transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
|
||||
transition-delay: 0ms !important;\
|
||||
transition-timing-function: ease-out !important;\
|
||||
@ -907,20 +908,16 @@ let StyleSheetActor = protocol.ActorClass({
|
||||
* to remove the rule after a certain time.
|
||||
*/
|
||||
_insertTransistionRule: function() {
|
||||
// Insert the global transition rule
|
||||
// Use a ref count to make sure we do not add it multiple times.. and remove
|
||||
// it only when all pending StyleSheets-generated transitions ended.
|
||||
if (this._transitionRefCount == 0) {
|
||||
this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
|
||||
this.document.documentElement.classList.add(TRANSITION_CLASS);
|
||||
}
|
||||
this.document.documentElement.classList.add(TRANSITION_CLASS);
|
||||
|
||||
this._transitionRefCount++;
|
||||
// We always add the rule since we've just reset all the rules
|
||||
this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
|
||||
|
||||
// Set up clean up and commit after transition duration (+buffer)
|
||||
// @see _onTransitionEnd
|
||||
this.window.setTimeout(this._onTransitionEnd.bind(this),
|
||||
TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS);
|
||||
this.window.clearTimeout(this._transitionTimeout);
|
||||
this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this),
|
||||
TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -929,9 +926,12 @@ let StyleSheetActor = protocol.ActorClass({
|
||||
*/
|
||||
_onTransitionEnd: function()
|
||||
{
|
||||
if (--this._transitionRefCount == 0) {
|
||||
this.document.documentElement.classList.remove(TRANSITION_CLASS);
|
||||
this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
|
||||
this.document.documentElement.classList.remove(TRANSITION_CLASS);
|
||||
|
||||
let index = this.rawSheet.cssRules.length - 1;
|
||||
let rule = this.rawSheet.cssRules[index];
|
||||
if (rule.selectorText == TRANSITION_RULE_SELECTOR) {
|
||||
this.rawSheet.deleteRule(index);
|
||||
}
|
||||
|
||||
events.emit(this, "style-applied");
|
||||
|
@ -1112,7 +1112,7 @@ let Front = Class({
|
||||
let message = (packet.error == "unknownError" && packet.message) ?
|
||||
"Protocol error: " + packet.message :
|
||||
packet.error;
|
||||
deferred.reject(packet.error);
|
||||
deferred.reject(message);
|
||||
} else {
|
||||
deferred.resolve(packet);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user