Bug 1088672 - Part 7. Rewrite Loop's incoming call handling in the flux style. Remove the now redundant non-flux based code for incoming calls. r=mikedeboer

This commit is contained in:
Mark Banner 2015-03-12 14:01:38 +00:00
parent fcf1584a61
commit 93876de832
10 changed files with 2 additions and 1250 deletions

View File

@ -26,7 +26,6 @@
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/models.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>

View File

@ -75,37 +75,6 @@ loop.Client = (function($) {
cb(error);
},
/**
* Block call URL based on the token identifier
*
* @param {string} token Conversation identifier used to block the URL
* @param {mozLoop.LOOP_SESSION_TYPE} sessionType The type of session which
* the url belongs to.
* @param {function} cb Callback function used for handling an error
* response. XXX The incoming call panel does not
* exist after the block button is clicked therefore
* it does not make sense to display an error.
**/
deleteCallUrl: function(token, sessionType, cb) {
function deleteRequestCallback(error, responseText) {
if (error) {
this._failureHandler(cb, error);
return;
}
try {
cb(null);
} catch (err) {
console.log("Error deleting call info", err);
cb(err);
}
}
this.mozLoop.hawkRequest(sessionType,
"/call-url/" + token, "DELETE", null,
deleteRequestCallback.bind(this));
},
/**
* Sets up an outgoing call, getting the relevant data from the server.
*

View File

@ -16,7 +16,6 @@ loop.conversation = (function(mozL10n) {
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var IncomingConversationView = loop.conversationViews.IncomingConversationView;
var CallControllerView = loop.conversationViews.CallControllerView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;

View File

@ -16,7 +16,6 @@ loop.conversation = (function(mozL10n) {
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var IncomingConversationView = loop.conversationViews.IncomingConversationView;
var CallControllerView = loop.conversationViews.CallControllerView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;

View File

@ -69,12 +69,7 @@ loop.store.ConversationAppStore = (function() {
return;
}
// XXX windowData is a hack for the IncomingConversationView until
// we rework it for the flux model in bug 1088672.
this.setStoreState({
windowType: windowData.type,
windowData: windowData
});
this.setStoreState({windowType: windowData.type});
this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData(_.extend({
windowId: actionData.windowId}, windowData)));

View File

@ -314,9 +314,6 @@ loop.conversationViews = (function(mozL10n) {
/**
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var GenericFailureView = React.createClass({displayName: "GenericFailureView",
mixins: [sharedMixins.AudioMixin],
@ -347,327 +344,6 @@ loop.conversationViews = (function(mozL10n) {
}
});
/**
* This view manages the incoming conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({displayName: "IncomingConversationView",
mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
isDesktop: React.PropTypes.bool,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired
},
getDefaultProps: function() {
return {
isDesktop: false
};
},
getInitialState: function() {
return {
callFailed: false, // XXX this should be removed when bug 1047410 lands.
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("accept", this.accept, this);
this.props.conversation.on("decline", this.decline, this);
this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
this.props.conversation.on("call:accepted", this.accepted, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this.endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
this.setupIncomingCall();
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
render: function() {
switch (this.state.callStatus) {
case "start": {
document.title = mozL10n.get("incoming_call_title2");
// XXX Don't render anything initially, though this should probably
// be some sort of pending view, whilst we connect the websocket.
return null;
}
case "incoming": {
document.title = mozL10n.get("incoming_call_title2");
return (
React.createElement(AcceptCallView, {
model: this.props.conversation,
video: this.props.conversation.hasVideoStream("incoming")}
)
);
}
case "connected": {
document.title = this.props.conversation.getCallIdentifier();
var callType = this.props.conversation.get("selectedCallType");
return (
React.createElement(sharedViews.ConversationView, {
isDesktop: this.props.isDesktop,
initiate: true,
sdk: this.props.sdk,
model: this.props.conversation,
video: {enabled: callType !== "audio"}}
)
);
}
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return React.createElement(GenericFailureView, {
cancelCall: this.closeWindow.bind(this)}
);
}
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
React.createElement(sharedViews.FeedbackView, {
onAfterFeedbackReceived: this.closeWindow.bind(this)}
)
);
}
case "close": {
this.closeWindow();
return (React.createElement("div", null));
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error(error);
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.setState({callFailed: false, callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Incoming call route.
*/
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
/**
* Starts the actual conversation
*/
accepted: function() {
this.setState({callStatus: "connected"});
},
/**
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function(progressStatus) {
this.setState({
callStatus: progressStatus === "terminated" ? "close" : "incoming"
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
* If we add more cases here, then we should refactor this function.
*
* @param {Object} progressData The progress data from the websocket.
* @param {String} previousState The previous state from the websocket.
*/
_handleWebSocketProgress: function(progressData, previousState) {
// We only care about the terminated state at the moment.
if (progressData.state !== "terminated")
return;
// XXX This would be nicer in the _abortIncomingCall function, but we need to stop
// it here for now due to server-side issues that are being fixed in bug 1088351.
// This is before the abort call to ensure that it happens before the window is
// closed.
navigator.mozLoop.stopAlerting();
// If we hit any of the termination reasons, and the user hasn't accepted
// then it seems reasonable to close the window/abort the incoming call.
//
// If the user has accepted the call, and something's happened, display
// the call failed view.
//
// https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
if (previousState === "init" || previousState === "alerting") {
this._abortIncomingCall();
} else {
this.setState({callFailed: true, callStatus: "end"});
}
},
/**
* Silently aborts an incoming call - stops the alerting, and
* closes the websocket.
*/
_abortIncomingCall: function() {
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._websocket.accept();
this.props.conversation.accepted();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error("Failed initiating the call session.");
},
});
/**
* View for pending conversations. Displays a cancel button and appropriate
* pending/ringing strings.
@ -1083,7 +759,6 @@ loop.conversationViews = (function(mozL10n) {
_getContactDisplayName: _getContactDisplayName,
GenericFailureView: GenericFailureView,
AcceptCallView: AcceptCallView,
IncomingConversationView: IncomingConversationView,
OngoingConversationView: OngoingConversationView,
CallControllerView: CallControllerView
};

View File

@ -314,9 +314,6 @@ loop.conversationViews = (function(mozL10n) {
/**
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var GenericFailureView = React.createClass({
mixins: [sharedMixins.AudioMixin],
@ -347,327 +344,6 @@ loop.conversationViews = (function(mozL10n) {
}
});
/**
* This view manages the incoming conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
isDesktop: React.PropTypes.bool,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired
},
getDefaultProps: function() {
return {
isDesktop: false
};
},
getInitialState: function() {
return {
callFailed: false, // XXX this should be removed when bug 1047410 lands.
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("accept", this.accept, this);
this.props.conversation.on("decline", this.decline, this);
this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
this.props.conversation.on("call:accepted", this.accepted, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this.endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
this.setupIncomingCall();
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
render: function() {
switch (this.state.callStatus) {
case "start": {
document.title = mozL10n.get("incoming_call_title2");
// XXX Don't render anything initially, though this should probably
// be some sort of pending view, whilst we connect the websocket.
return null;
}
case "incoming": {
document.title = mozL10n.get("incoming_call_title2");
return (
<AcceptCallView
model={this.props.conversation}
video={this.props.conversation.hasVideoStream("incoming")}
/>
);
}
case "connected": {
document.title = this.props.conversation.getCallIdentifier();
var callType = this.props.conversation.get("selectedCallType");
return (
<sharedViews.ConversationView
isDesktop={this.props.isDesktop}
initiate={true}
sdk={this.props.sdk}
model={this.props.conversation}
video={{enabled: callType !== "audio"}}
/>
);
}
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return <GenericFailureView
cancelCall={this.closeWindow.bind(this)}
/>;
}
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
<sharedViews.FeedbackView
onAfterFeedbackReceived={this.closeWindow.bind(this)}
/>
);
}
case "close": {
this.closeWindow();
return (<div/>);
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error(error);
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.setState({callFailed: false, callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Incoming call route.
*/
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
/**
* Starts the actual conversation
*/
accepted: function() {
this.setState({callStatus: "connected"});
},
/**
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function(progressStatus) {
this.setState({
callStatus: progressStatus === "terminated" ? "close" : "incoming"
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
* If we add more cases here, then we should refactor this function.
*
* @param {Object} progressData The progress data from the websocket.
* @param {String} previousState The previous state from the websocket.
*/
_handleWebSocketProgress: function(progressData, previousState) {
// We only care about the terminated state at the moment.
if (progressData.state !== "terminated")
return;
// XXX This would be nicer in the _abortIncomingCall function, but we need to stop
// it here for now due to server-side issues that are being fixed in bug 1088351.
// This is before the abort call to ensure that it happens before the window is
// closed.
navigator.mozLoop.stopAlerting();
// If we hit any of the termination reasons, and the user hasn't accepted
// then it seems reasonable to close the window/abort the incoming call.
//
// If the user has accepted the call, and something's happened, display
// the call failed view.
//
// https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
if (previousState === "init" || previousState === "alerting") {
this._abortIncomingCall();
} else {
this.setState({callFailed: true, callStatus: "end"});
}
},
/**
* Silently aborts an incoming call - stops the alerting, and
* closes the websocket.
*/
_abortIncomingCall: function() {
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._websocket.accept();
this.props.conversation.accepted();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error("Failed initiating the call session.");
},
});
/**
* View for pending conversations. Displays a cancel button and appropriate
* pending/ringing strings.
@ -1083,7 +759,6 @@ loop.conversationViews = (function(mozL10n) {
_getContactDisplayName: _getContactDisplayName,
GenericFailureView: GenericFailureView,
AcceptCallView: AcceptCallView,
IncomingConversationView: IncomingConversationView,
OngoingConversationView: OngoingConversationView,
CallControllerView: CallControllerView
};

View File

@ -52,42 +52,6 @@ describe("loop.Client", function() {
});
describe("loop.Client", function() {
describe("#deleteCallUrl", function() {
it("should make a delete call to /call-url/{fakeToken}", function() {
client.deleteCallUrl(fakeToken, mozLoop.LOOP_SESSION_TYPE.GUEST, callback);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWith(hawkRequestStub,
mozLoop.LOOP_SESSION_TYPE.GUEST,
"/call-url/" + fakeToken, "DELETE");
});
it("should call the callback with null when the request succeeds",
function() {
// Sets up the hawkRequest stub to trigger the callback with no error
// and the url.
hawkRequestStub.callsArgWith(4, null);
client.deleteCallUrl(fakeToken, mozLoop.LOOP_SESSION_TYPE.FXA, callback);
sinon.assert.calledWithExactly(callback, null);
});
it("should send an error when the request fails", function() {
// Sets up the hawkRequest stub to trigger the callback with
// an error
hawkRequestStub.callsArgWith(4, fakeErrorRes);
client.deleteCallUrl(fakeToken, mozLoop.LOOP_SESSION_TYPE.FXA, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return err.code == 400 && "invalid token" == err.message;
}));
});
});
describe("#setupOutgoingCall", function() {
var calleeIds, callType;

View File

@ -63,8 +63,7 @@ describe("loop.store.ConversationAppStore", function () {
dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
expect(store.getStoreState()).eql({
windowType: "incoming",
windowData: fakeWindowData
windowType: "incoming"
});
});

View File

@ -718,528 +718,6 @@ describe("loop.conversationViews", function () {
});
});
describe("IncomingConversationView", function() {
var conversationAppStore, conversation, client, icView, oldTitle,
feedbackStore;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.IncomingConversationView, {
client: client,
conversation: conversation,
sdk: {},
conversationAppStore: conversationAppStore
}));
}
beforeEach(function() {
oldTitle = document.title;
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {}
});
conversation.set({windowId: 42});
var dispatcher = new loop.Dispatcher();
conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
sandbox.stub(conversation, "setOutgoingSessionData");
});
afterEach(function() {
icView = undefined;
document.title = oldTitle;
});
describe("start", function() {
it("should set the title to incoming_call_title2", function() {
conversationAppStore.setStoreState({
windowData: {
progressURL: "fake",
websocketToken: "fake",
callId: 42
}
});
icView = mountTestComponent();
expect(document.title).eql("incoming_call_title2");
});
});
describe("componentDidMount", function() {
var fakeSessionData, promise, resolveWebSocketConnect;
var rejectWebSocketConnect;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: "7b"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
stubComponent(loop.conversationViews, "AcceptCallView");
stubComponent(sharedView, "ConversationView");
});
it("should start alerting", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
});
describe("Session Data setup", function() {
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function () {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
return promise;
},
on: sinon.stub()
});
});
it("should store the session data", function() {
sandbox.stub(conversation, "setIncomingSessionData");
icView = mountTestComponent();
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
it("should setup the websocket connection", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
websocketToken: "7b"
});
});
});
describe("WebSocket Handling", function() {
beforeEach(function() {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
});
it("should set the state to incoming on success", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("incoming");
promise.then(function () {
expect(icView.state.callStatus).eql("incoming");
done();
});
});
it("should set the state to close on success if the progress " +
"state is terminated", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("terminated");
promise.then(function () {
expect(icView.state.callStatus).eql("close");
done();
});
});
// XXX implement me as part of bug 1047410
// see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
it.skip("should should switch view state to failed", function(done) {
icView = mountTestComponent();
rejectWebSocketConnect();
promise.then(function() {}, function() {
done();
});
});
});
describe("WebSocket Events", function() {
describe("Call cancelled or timed out before acceptance", function() {
beforeEach(function() {
// Mounting the test component automatically calls the required
// setup functions
icView = mountTestComponent();
promise = new Promise(function(resolve, reject) {
resolve();
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
});
describe("progress - terminated (previousState = alerting)", function() {
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: WEBSOCKET_REASONS.TIMEOUT
}, "alerting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
it("should close the websocket", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: WEBSOCKET_REASONS.CLOSED
}, "alerting");
sinon.assert.calledOnce(icView._websocket.close);
done();
});
});
it("should close the window", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: WEBSOCKET_REASONS.ANSWERED_ELSEWHERE
}, "alerting");
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
done();
});
});
});
describe("progress - terminated (previousState not init" +
" nor alerting)",
function() {
it("should set the state to end", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: WEBSOCKET_REASONS.MEDIA_FAIL
}, "connecting");
expect(icView.state.callStatus).eql("end");
done();
});
});
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: WEBSOCKET_REASONS.MEDIA_FAIL
}, "connecting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
});
});
});
describe("#accept", function() {
beforeEach(function() {
icView = mountTestComponent();
conversation.setIncomingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
sandbox.stub(icView._websocket, "accept");
sandbox.stub(icView.props.conversation, "accepted");
});
it("should initiate the conversation", function() {
icView.accept();
sinon.assert.calledOnce(icView.props.conversation.accepted);
});
it("should notify the websocket of the user acceptance", function() {
icView.accept();
sinon.assert.calledOnce(icView._websocket.accept);
});
it("should stop alerting", function() {
icView.accept();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
});
describe("#decline", function() {
beforeEach(function() {
icView = mountTestComponent();
icView._websocket = {
decline: sinon.stub(),
close: sinon.stub()
};
conversation.set({
windowId: "8699"
});
conversation.setIncomingSessionData({
websocketToken: 123
});
});
it("should close the window", function() {
icView.decline();
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
});
it("should stop alerting", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should release callData", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "8699");
});
});
describe("#blocked", function() {
var mozLoop, deleteCallUrlStub;
beforeEach(function() {
icView = mountTestComponent();
icView._websocket = {
decline: sinon.spy(),
close: sinon.stub()
};
mozLoop = {
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2
}
};
deleteCallUrlStub = sandbox.stub(loop.Client.prototype,
"deleteCallUrl");
});
it("should call mozLoop.stopAlerting", function() {
icView.declineAndBlock();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should call delete call", function() {
sandbox.stub(conversation, "get").withArgs("callToken")
.returns("fakeToken")
.withArgs("sessionType")
.returns(mozLoop.LOOP_SESSION_TYPE.FXA);
icView.declineAndBlock();
sinon.assert.calledOnce(deleteCallUrlStub);
sinon.assert.calledWithExactly(deleteCallUrlStub,
"fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func);
});
it("should get callToken from conversation model", function() {
sandbox.stub(conversation, "get");
icView.declineAndBlock();
sinon.assert.called(conversation.get);
sinon.assert.calledWithExactly(conversation.get, "callToken");
sinon.assert.calledWithExactly(conversation.get, "windowId");
});
it("should trigger error handling in case of error", function() {
// XXX just logging to console for now
var log = sandbox.stub(console, "log");
var fakeError = {
error: true
};
deleteCallUrlStub.callsArgWith(2, fakeError);
icView.declineAndBlock();
sinon.assert.calledOnce(log);
sinon.assert.calledWithExactly(log, fakeError);
});
it("should close the window", function() {
icView.declineAndBlock();
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
});
});
});
describe("Events", function() {
var fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
return new Promise(function() {});
},
on: sandbox.spy()
});
icView = mountTestComponent();
conversation.set("loopToken", "fakeToken");
stubComponent(sharedView, "ConversationView");
});
describe("call:accepted", function() {
it("should display the ConversationView",
function() {
conversation.accepted();
TestUtils.findRenderedComponentWithType(icView,
sharedView.ConversationView);
});
it("should set the title to the call identifier", function() {
sandbox.stub(conversation, "getCallIdentifier").returns("fakeId");
conversation.accepted();
expect(document.title).eql("fakeId");
});
});
describe("session:ended", function() {
it("should display the feedback view when the call session ends",
function() {
conversation.trigger("session:ended");
TestUtils.findRenderedComponentWithType(icView,
sharedView.FeedbackView);
});
});
describe("session:peer-hungup", function() {
it("should display the feedback view when the peer hangs up",
function() {
conversation.trigger("session:peer-hungup");
TestUtils.findRenderedComponentWithType(icView,
sharedView.FeedbackView);
});
});
describe("session:network-disconnected", function() {
it("should navigate to call failed when network disconnects",
function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(icView,
loop.conversationViews.GenericFailureView);
});
it("should update the conversation window toolbar title",
function() {
conversation.trigger("session:network-disconnected");
expect(document.title).eql("generic_failure_title");
});
});
describe("Published and Subscribed Streams", function() {
beforeEach(function() {
icView._websocket = {
mediaUp: sinon.spy()
};
});
describe("publishStream", function() {
it("should not notify the websocket if only one stream is up",
function() {
conversation.set("publishedStream", true);
sinon.assert.notCalled(icView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
"are connected", function() {
conversation.set("subscribedStream", true);
conversation.set("publishedStream", true);
sinon.assert.calledOnce(icView._websocket.mediaUp);
});
});
describe("subscribedStream", function() {
it("should not notify the websocket if only one stream is up",
function() {
conversation.set("subscribedStream", true);
sinon.assert.notCalled(icView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
"are connected", function() {
conversation.set("publishedStream", true);
conversation.set("subscribedStream", true);
sinon.assert.calledOnce(icView._websocket.mediaUp);
});
});
});
});
});
describe("AcceptCallView", function() {
var view;