mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-01 17:23:59 +00:00
Bug 972992 (Part 2): Loop desktop feedback add an API client to actually submit feedback. r=Standard8
This commit is contained in:
parent
96c36a5dc5
commit
318c8e3cc7
@ -1579,6 +1579,8 @@ 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/");
|
||||
|
@ -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>
|
||||
|
@ -131,7 +131,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"call/accept": "accept",
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended",
|
||||
"call/declineAndBlock": "declineAndBlock",
|
||||
"call/feedback": "feedback"
|
||||
},
|
||||
@ -157,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});
|
||||
@ -177,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
|
||||
@ -188,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();
|
||||
},
|
||||
@ -200,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) {
|
||||
@ -234,13 +233,13 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* Call has ended, display a feedback form.
|
||||
*/
|
||||
feedback: function() {
|
||||
document.title = mozL10n.get("call_has_ended");
|
||||
|
||||
this.loadReactComponent(sharedViews.FeedbackView({
|
||||
// XXX for now we pass in a fake feeback API client (see bug 972992)
|
||||
feedbackApiClient: {
|
||||
send: function(fields, cb) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
feedbackApiClient: new loop.FeedbackAPIClient({
|
||||
baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
|
||||
product: navigator.mozLoop.getLoopCharPref("feedback.product")
|
||||
})
|
||||
}));
|
||||
}
|
||||
});
|
||||
@ -251,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");
|
||||
|
||||
|
@ -131,7 +131,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"call/accept": "accept",
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended",
|
||||
"call/declineAndBlock": "declineAndBlock",
|
||||
"call/feedback": "feedback"
|
||||
},
|
||||
@ -157,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});
|
||||
@ -177,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
|
||||
@ -188,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();
|
||||
},
|
||||
@ -200,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) {
|
||||
@ -234,13 +233,13 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* Call has ended, display a feedback form.
|
||||
*/
|
||||
feedback: function() {
|
||||
document.title = mozL10n.get("call_has_ended");
|
||||
|
||||
this.loadReactComponent(sharedViews.FeedbackView({
|
||||
// XXX for now we pass in a fake feeback API client (see bug 972992)
|
||||
feedbackApiClient: {
|
||||
send: function(fields, cb) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
feedbackApiClient: new loop.FeedbackAPIClient({
|
||||
baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
|
||||
product: navigator.mozLoop.getLoopCharPref("feedback.product")
|
||||
})
|
||||
}));
|
||||
}
|
||||
});
|
||||
@ -251,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");
|
||||
|
||||
|
@ -77,22 +77,22 @@ loop.panel = (function(_, mozL10n) {
|
||||
__("display_name_available_status");
|
||||
|
||||
return (
|
||||
React.DOM.div({className: "footer component-spacer"},
|
||||
React.DOM.div({className: "do-not-disturb"},
|
||||
React.DOM.p({className: "dnd-status", onClick: this.showDropdownMenu},
|
||||
React.DOM.span(null, availabilityText),
|
||||
React.DOM.i({className: availabilityStatus})
|
||||
),
|
||||
React.DOM.ul({className: availabilityDropdown,
|
||||
onMouseLeave: this.hideDropdownMenu},
|
||||
React.DOM.li({onClick: this.changeAvailability("available"),
|
||||
className: "dnd-menu-item dnd-make-available"},
|
||||
React.DOM.i({className: "status status-available"}),
|
||||
React.DOM.div( {className:"footer component-spacer"},
|
||||
React.DOM.div( {className:"do-not-disturb"},
|
||||
React.DOM.p( {className:"dnd-status", onClick:this.showDropdownMenu},
|
||||
React.DOM.span(null, availabilityText),
|
||||
React.DOM.i( {className:availabilityStatus})
|
||||
),
|
||||
React.DOM.ul( {className:availabilityDropdown,
|
||||
onMouseLeave:this.hideDropdownMenu},
|
||||
React.DOM.li( {onClick:this.changeAvailability("available"),
|
||||
className:"dnd-menu-item dnd-make-available"},
|
||||
React.DOM.i( {className:"status status-available"}),
|
||||
React.DOM.span(null, __("display_name_available_status"))
|
||||
),
|
||||
React.DOM.li({onClick: this.changeAvailability("do-not-disturb"),
|
||||
className: "dnd-menu-item dnd-make-unavailable"},
|
||||
React.DOM.i({className: "status status-dnd"}),
|
||||
),
|
||||
React.DOM.li( {onClick:this.changeAvailability("do-not-disturb"),
|
||||
className:"dnd-menu-item dnd-make-unavailable"},
|
||||
React.DOM.i( {className:"status status-dnd"}),
|
||||
React.DOM.span(null, __("display_name_dnd_status"))
|
||||
)
|
||||
)
|
||||
@ -115,10 +115,10 @@ loop.panel = (function(_, mozL10n) {
|
||||
|
||||
if (this.state.seenToS == "unseen") {
|
||||
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
||||
return React.DOM.p({className: "terms-service",
|
||||
dangerouslySetInnerHTML: {__html: tosHTML}});
|
||||
return React.DOM.p( {className:"terms-service",
|
||||
dangerouslySetInnerHTML:{__html: tosHTML}});
|
||||
} else {
|
||||
return React.DOM.div(null);
|
||||
return React.DOM.div(null );
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -130,11 +130,11 @@ loop.panel = (function(_, mozL10n) {
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div({className: "component-spacer share generate-url"},
|
||||
React.DOM.div({className: "description"},
|
||||
React.DOM.p({className: "description-content"}, this.props.summary)
|
||||
),
|
||||
React.DOM.div({className: "action"},
|
||||
React.DOM.div( {className:"component-spacer share generate-url"},
|
||||
React.DOM.div( {className:"description"},
|
||||
React.DOM.p( {className:"description-content"}, this.props.summary)
|
||||
),
|
||||
React.DOM.div( {className:"action"},
|
||||
this.props.children
|
||||
)
|
||||
)
|
||||
@ -201,10 +201,10 @@ loop.panel = (function(_, mozL10n) {
|
||||
// from the react lib.
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
PanelLayout({summary: __("share_link_header_text")},
|
||||
React.DOM.div({className: "invite"},
|
||||
React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true",
|
||||
className: cx({'pending': this.state.pending})})
|
||||
PanelLayout( {summary:__("share_link_header_text")},
|
||||
React.DOM.div( {className:"invite"},
|
||||
React.DOM.input( {type:"url", value:this.state.callUrl, readOnly:"true",
|
||||
className:cx({'pending': this.state.pending})} )
|
||||
)
|
||||
)
|
||||
);
|
||||
@ -223,10 +223,10 @@ loop.panel = (function(_, mozL10n) {
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div(null,
|
||||
CallUrlResult({client: this.props.client,
|
||||
notifier: this.props.notifier}),
|
||||
ToSView(null),
|
||||
AvailabilityDropdown(null)
|
||||
CallUrlResult( {client:this.props.client,
|
||||
notifier:this.props.notifier} ),
|
||||
ToSView(null ),
|
||||
AvailabilityDropdown(null )
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -293,8 +293,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
var client = new loop.Client({
|
||||
baseServerUrl: navigator.mozLoop.serverUrl
|
||||
});
|
||||
this.loadReactComponent(PanelView({client: client,
|
||||
notifier: this._notifier}));
|
||||
this.loadReactComponent(PanelView( {client:client,
|
||||
notifier:this._notifier} ));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
},
|
||||
|
||||
|
@ -396,34 +396,67 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {reason: "", custom: ""};
|
||||
return {category: "", description: ""};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {pending: false};
|
||||
},
|
||||
|
||||
handleReasonChange: function(event) {
|
||||
var reason = event.target.value;
|
||||
if (reason === "other") {
|
||||
this.setState({reason: reason});
|
||||
} else {
|
||||
// resets custom text field
|
||||
this.setState({custom: ""});
|
||||
_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({custom: event.target.value});
|
||||
this.setState({description: event.target.value});
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
var reason = this.refs.reason.getDOMNode().value.trim();
|
||||
var custom = this.refs.custom.getDOMNode().value.trim();
|
||||
this.props.sendFeedback({
|
||||
reason: reason,
|
||||
custom: reason === "other" ? custom : ""
|
||||
happy: false,
|
||||
category: this.state.category,
|
||||
description: this.state.description
|
||||
});
|
||||
},
|
||||
|
||||
@ -432,37 +465,13 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
FeedbackLayout( {title:__("feedback_what_makes_you_sad"),
|
||||
reset:this.props.reset},
|
||||
React.DOM.form( {onSubmit:this.handleFormSubmit},
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason",
|
||||
value:"audio_quality", onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_audio_quality")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason",
|
||||
value:"video_quality", onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_video_quality")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason",
|
||||
value:"disconnected", onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_was_disconnected")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason", value:"confusing",
|
||||
onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_confusing")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason", value:"other",
|
||||
onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_other")
|
||||
),
|
||||
React.DOM.p(null, React.DOM.input( {type:"text", ref:"custom", name:"custom",
|
||||
disabled:this.state.reason !== "other",
|
||||
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.custom} )),
|
||||
value:this.state.description} )),
|
||||
React.DOM.button( {type:"submit", className:"btn btn-success",
|
||||
disabled:this.props.pending},
|
||||
disabled:!this._isFormReady()},
|
||||
__("feedback_submit_button")
|
||||
)
|
||||
)
|
||||
@ -530,7 +539,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
},
|
||||
|
||||
handleHappyClick: function() {
|
||||
this.setState({step: "finished"});
|
||||
this.sendFeedback({happy: true}, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
handleSadClick: function() {
|
||||
|
@ -396,34 +396,67 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {reason: "", custom: ""};
|
||||
return {category: "", description: ""};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {pending: false};
|
||||
},
|
||||
|
||||
handleReasonChange: function(event) {
|
||||
var reason = event.target.value;
|
||||
if (reason === "other") {
|
||||
this.setState({reason: reason});
|
||||
} else {
|
||||
// resets custom text field
|
||||
this.setState({custom: ""});
|
||||
_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({custom: event.target.value});
|
||||
this.setState({description: event.target.value});
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
var reason = this.refs.reason.getDOMNode().value.trim();
|
||||
var custom = this.refs.custom.getDOMNode().value.trim();
|
||||
this.props.sendFeedback({
|
||||
reason: reason,
|
||||
custom: reason === "other" ? custom : ""
|
||||
happy: false,
|
||||
category: this.state.category,
|
||||
description: this.state.description
|
||||
});
|
||||
},
|
||||
|
||||
@ -432,37 +465,13 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
<FeedbackLayout title={__("feedback_what_makes_you_sad")}
|
||||
reset={this.props.reset}>
|
||||
<form onSubmit={this.handleFormSubmit}>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason"
|
||||
value="audio_quality" onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_audio_quality")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason"
|
||||
value="video_quality" onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_video_quality")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason"
|
||||
value="disconnected" onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_was_disconnected")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason" value="confusing"
|
||||
onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_confusing")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason" value="other"
|
||||
onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_other")}
|
||||
</label>
|
||||
<p><input type="text" ref="custom" name="custom"
|
||||
disabled={this.state.reason !== "other"}
|
||||
{this._getCategoryFields()}
|
||||
<p><input type="text" ref="description" name="description"
|
||||
disabled={this.state.category !== "other"}
|
||||
onChange={this.handleCustomTextChange}
|
||||
value={this.state.custom} /></p>
|
||||
value={this.state.description} /></p>
|
||||
<button type="submit" className="btn btn-success"
|
||||
disabled={this.props.pending}>
|
||||
disabled={!this._isFormReady()}>
|
||||
{__("feedback_submit_button")}
|
||||
</button>
|
||||
</form>
|
||||
@ -530,7 +539,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
},
|
||||
|
||||
handleHappyClick: function() {
|
||||
this.setState({step: "finished"});
|
||||
this.sendFeedback({happy: true}, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
handleSadClick: function() {
|
||||
|
@ -41,10 +41,11 @@ 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)
|
||||
|
@ -100,15 +100,15 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
React.DOM.header({className: "container-box"},
|
||||
React.DOM.h1({className: "light-weight-font"},
|
||||
React.DOM.header( {className:"container-box"},
|
||||
React.DOM.h1( {className:"light-weight-font"},
|
||||
React.DOM.strong(null, __("brandShortname")), " ", __("clientShortname")
|
||||
),
|
||||
React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}),
|
||||
React.DOM.h3({className: "call-url"},
|
||||
),
|
||||
React.DOM.div( {className:"loop-logo", title:"Firefox WebRTC! logo"}),
|
||||
React.DOM.h3( {className:"call-url"},
|
||||
conversationUrl
|
||||
),
|
||||
React.DOM.h4({className: urlCreationDateClasses},
|
||||
),
|
||||
React.DOM.h4( {className:urlCreationDateClasses} ,
|
||||
callUrlCreationDateString
|
||||
)
|
||||
)
|
||||
@ -120,8 +120,8 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
var ConversationFooter = React.createClass({displayName: 'ConversationFooter',
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div({className: "footer container-box"},
|
||||
React.DOM.div({title: "Mozilla Logo", className: "footer-logo"})
|
||||
React.DOM.div( {className:"footer container-box"},
|
||||
React.DOM.div( {title:"Mozilla Logo", className:"footer-logo"})
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -217,34 +217,34 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
React.DOM.div({className: "container"},
|
||||
React.DOM.div({className: "container-box"},
|
||||
React.DOM.div( {className:"container"},
|
||||
React.DOM.div( {className:"container-box"},
|
||||
|
||||
ConversationHeader({
|
||||
urlCreationDateString: this.state.urlCreationDateString}),
|
||||
ConversationHeader(
|
||||
{urlCreationDateString:this.state.urlCreationDateString} ),
|
||||
|
||||
React.DOM.p({className: "large-font light-weight-font"},
|
||||
React.DOM.p( {className:"large-font light-weight-font"},
|
||||
__("initiate_call_button_label")
|
||||
),
|
||||
),
|
||||
|
||||
React.DOM.div({id: "messages"}),
|
||||
React.DOM.div( {id:"messages"}),
|
||||
|
||||
React.DOM.div({className: "button-group"},
|
||||
React.DOM.div({className: "flex-padding-1"}),
|
||||
React.DOM.button({ref: "submitButton", onClick: this._initiate,
|
||||
className: callButtonClasses,
|
||||
disabled: this.state.disableCallButton},
|
||||
__("initiate_call_button"),
|
||||
React.DOM.i({className: "icon icon-video"})
|
||||
),
|
||||
React.DOM.div({className: "flex-padding-1"})
|
||||
),
|
||||
React.DOM.div( {className:"button-group"},
|
||||
React.DOM.div( {className:"flex-padding-1"}),
|
||||
React.DOM.button( {ref:"submitButton", onClick:this._initiate,
|
||||
className:callButtonClasses,
|
||||
disabled:this.state.disableCallButton},
|
||||
__("initiate_call_button"),
|
||||
React.DOM.i( {className:"icon icon-video"})
|
||||
),
|
||||
React.DOM.div( {className:"flex-padding-1"})
|
||||
),
|
||||
|
||||
React.DOM.p({className: "terms-service",
|
||||
dangerouslySetInnerHTML: {__html: tosHTML}})
|
||||
),
|
||||
React.DOM.p( {className:"terms-service",
|
||||
dangerouslySetInnerHTML:{__html: tosHTML}})
|
||||
),
|
||||
|
||||
ConversationFooter(null)
|
||||
ConversationFooter(null )
|
||||
)
|
||||
/* jshint ignore:end */
|
||||
);
|
||||
|
@ -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() {
|
||||
@ -165,10 +165,10 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -241,18 +241,30 @@ 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("#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 (see bug 1046744)
|
||||
it("should display a feedback form view", function() {
|
||||
@ -265,14 +277,20 @@ describe("loop.conversation", function() {
|
||||
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() {
|
||||
|
@ -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>
|
||||
|
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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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");
|
||||
|
||||
|
@ -411,76 +411,131 @@ describe("loop.shared.views", function() {
|
||||
}));
|
||||
});
|
||||
|
||||
it("should thank the user for feedback when clicking on the happy face",
|
||||
function() {
|
||||
var happyFace = comp.getDOMNode().querySelector(".face-happy");
|
||||
// local test helpers
|
||||
function clickHappyFace(comp) {
|
||||
var happyFace = comp.getDOMNode().querySelector(".face-happy");
|
||||
TestUtils.Simulate.click(happyFace);
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("should bring the user to feedback form when clicking on the sad face",
|
||||
function() {
|
||||
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
||||
|
||||
TestUtils.Simulate.click(sadFace);
|
||||
|
||||
expect(comp.getDOMNode().querySelectorAll("form").length).eql(1);
|
||||
});
|
||||
|
||||
it("should disable the form submit button once the form is first submitted",
|
||||
function() {
|
||||
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
||||
TestUtils.Simulate.click(sadFace);
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='confusing']"), {
|
||||
target: {checked: true}
|
||||
});
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1033841#c10
|
||||
expect(comp.getDOMNode()
|
||||
.querySelector("form button")
|
||||
.getAttribute("disabled")).eql("true");
|
||||
});
|
||||
|
||||
it("should send feedback data over the feedback API client", function() {
|
||||
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".face-sad"));
|
||||
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='other']"), {
|
||||
target: {checked: true}
|
||||
});
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[name='custom']"), {
|
||||
target: {value: "fake"}
|
||||
});
|
||||
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
|
||||
sinon.assert.calledOnce(fakeFeedbackApiClient.send);
|
||||
sinon.assert.calledWith(fakeFeedbackApiClient.send, {
|
||||
reason: "other",
|
||||
custom: "fake"
|
||||
});
|
||||
});
|
||||
|
||||
it("should thank the user when feedback data has been sent", function() {
|
||||
fakeFeedbackApiClient.send = function(data, cb) {
|
||||
cb();
|
||||
};
|
||||
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".face-sad"));
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='confusing']"), {
|
||||
target: {checked: true}
|
||||
});
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
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(".feedback .thank-you").length).eql(1);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -33,13 +33,12 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// local fakes
|
||||
var fakeFeedbackApiClient = {
|
||||
send: function(fields, cb) {
|
||||
alert("Sent feedback data: " + JSON.stringify(fields));
|
||||
cb();
|
||||
}
|
||||
};
|
||||
// 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() {
|
||||
@ -124,8 +123,12 @@
|
||||
),
|
||||
|
||||
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:fakeFeedbackApiClient} )
|
||||
FeedbackView( {feedbackApiClient:stageFeedbackApiClient} )
|
||||
),
|
||||
Example( {summary:"Detailed form", dashed:"true", style:{width: "280px"}},
|
||||
FeedbackView( {step:"form"} )
|
||||
|
@ -33,13 +33,12 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// local fakes
|
||||
var fakeFeedbackApiClient = {
|
||||
send: function(fields, cb) {
|
||||
alert("Sent feedback data: " + JSON.stringify(fields));
|
||||
cb();
|
||||
}
|
||||
};
|
||||
// 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() {
|
||||
@ -124,8 +123,12 @@
|
||||
</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={fakeFeedbackApiClient} />
|
||||
<FeedbackView feedbackApiClient={stageFeedbackApiClient} />
|
||||
</Example>
|
||||
<Example summary="Detailed form" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView step="form" />
|
||||
|
@ -42,11 +42,11 @@ legal_text_and_links=By using this product you agree to the <a \
|
||||
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_reason_audio_quality=Audio quality
|
||||
feedback_reason_video_quality=Video quality
|
||||
feedback_reason_was_disconnected=Was disconnected
|
||||
feedback_reason_confusing=Confusing
|
||||
feedback_reason_other=Other:
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user