зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1088672 - Part 2. Rewrite Loop's incoming call handling in the flux style. Switch incoming calls to use flux based conversation store and get them working as far as the accept view. r=mikedeboer
This commit is contained in:
Родитель
3379e40e51
Коммит
99d6d0692a
|
@ -34,13 +34,6 @@ loop.conversation = (function(mozL10n) {
|
|||
],
|
||||
|
||||
propTypes: {
|
||||
// XXX Old types required for incoming call view.
|
||||
client: React.PropTypes.instanceOf(loop.Client).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
|
||||
// XXX New types for flux style
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
|
||||
},
|
||||
|
@ -51,15 +44,8 @@ loop.conversation = (function(mozL10n) {
|
|||
|
||||
render: function() {
|
||||
switch(this.state.windowType) {
|
||||
case "incoming": {
|
||||
return (React.createElement(IncomingConversationView, {
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
sdk: this.props.sdk,
|
||||
isDesktop: true,
|
||||
conversationAppStore: this.getStore()}
|
||||
));
|
||||
}
|
||||
// CallControllerView is used for both.
|
||||
case "incoming":
|
||||
case "outgoing": {
|
||||
return (React.createElement(CallControllerView, {
|
||||
dispatcher: this.props.dispatcher}
|
||||
|
@ -156,13 +142,6 @@ loop.conversation = (function(mozL10n) {
|
|||
feedbackStore: feedbackStore,
|
||||
});
|
||||
|
||||
// XXX Old class creation for the incoming conversation view, whilst
|
||||
// we transition across (bug 1072323).
|
||||
var conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: window.OT,
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
|
||||
// Obtain the windowId and pass it through
|
||||
var locationHash = loop.shared.utils.locationData().hash;
|
||||
var windowId;
|
||||
|
@ -172,8 +151,6 @@ loop.conversation = (function(mozL10n) {
|
|||
windowId = hash[1];
|
||||
}
|
||||
|
||||
conversation.set({windowId: windowId});
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// Handle direct close of dialog box via [x] control.
|
||||
// XXX Move to the conversation models, when we transition
|
||||
|
@ -185,10 +162,7 @@ loop.conversation = (function(mozL10n) {
|
|||
|
||||
React.render(React.createElement(AppControllerView, {
|
||||
roomStore: roomStore,
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
sdk: window.OT}
|
||||
dispatcher: dispatcher}
|
||||
), document.querySelector('#main'));
|
||||
|
||||
dispatcher.dispatch(new sharedActions.GetWindowData({
|
||||
|
|
|
@ -34,13 +34,6 @@ loop.conversation = (function(mozL10n) {
|
|||
],
|
||||
|
||||
propTypes: {
|
||||
// XXX Old types required for incoming call view.
|
||||
client: React.PropTypes.instanceOf(loop.Client).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
|
||||
// XXX New types for flux style
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
|
||||
},
|
||||
|
@ -51,15 +44,8 @@ loop.conversation = (function(mozL10n) {
|
|||
|
||||
render: function() {
|
||||
switch(this.state.windowType) {
|
||||
case "incoming": {
|
||||
return (<IncomingConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
sdk={this.props.sdk}
|
||||
isDesktop={true}
|
||||
conversationAppStore={this.getStore()}
|
||||
/>);
|
||||
}
|
||||
// CallControllerView is used for both.
|
||||
case "incoming":
|
||||
case "outgoing": {
|
||||
return (<CallControllerView
|
||||
dispatcher={this.props.dispatcher}
|
||||
|
@ -156,13 +142,6 @@ loop.conversation = (function(mozL10n) {
|
|||
feedbackStore: feedbackStore,
|
||||
});
|
||||
|
||||
// XXX Old class creation for the incoming conversation view, whilst
|
||||
// we transition across (bug 1072323).
|
||||
var conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: window.OT,
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
|
||||
// Obtain the windowId and pass it through
|
||||
var locationHash = loop.shared.utils.locationData().hash;
|
||||
var windowId;
|
||||
|
@ -172,8 +151,6 @@ loop.conversation = (function(mozL10n) {
|
|||
windowId = hash[1];
|
||||
}
|
||||
|
||||
conversation.set({windowId: windowId});
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// Handle direct close of dialog box via [x] control.
|
||||
// XXX Move to the conversation models, when we transition
|
||||
|
@ -185,10 +162,7 @@ loop.conversation = (function(mozL10n) {
|
|||
|
||||
React.render(<AppControllerView
|
||||
roomStore={roomStore}
|
||||
client={client}
|
||||
conversation={conversation}
|
||||
dispatcher={dispatcher}
|
||||
sdk={window.OT}
|
||||
/>, document.querySelector('#main'));
|
||||
|
||||
dispatcher.dispatch(new sharedActions.GetWindowData({
|
||||
|
|
|
@ -144,14 +144,16 @@ loop.conversationViews = (function(mozL10n) {
|
|||
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
|
||||
|
||||
propTypes: {
|
||||
model: React.PropTypes.object.isRequired,
|
||||
video: React.PropTypes.bool.isRequired
|
||||
callType: React.PropTypes.string.isRequired,
|
||||
callerId: React.PropTypes.string.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
// Only for use by the ui-showcase
|
||||
showMenu: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showMenu: false,
|
||||
video: true
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -221,9 +223,8 @@ loop.conversationViews = (function(mozL10n) {
|
|||
|
||||
return (
|
||||
React.createElement("div", {className: "call-window"},
|
||||
React.createElement(CallIdentifierView, {video: this.props.video,
|
||||
peerIdentifier: this.props.model.getCallIdentifier(),
|
||||
urlCreationDate: this.props.model.get("urlCreationDate"),
|
||||
React.createElement(CallIdentifierView, {video: this.props.callType === CALL_TYPES.AUDIO_VIDEO,
|
||||
peerIdentifier: this.props.callerId,
|
||||
showIcons: true}),
|
||||
|
||||
React.createElement("div", {className: "btn-group call-action-group"},
|
||||
|
@ -964,6 +965,33 @@ loop.conversationViews = (function(mozL10n) {
|
|||
);
|
||||
},
|
||||
|
||||
_renderViewFromCallType: function() {
|
||||
// For outgoing calls we can display the pending conversation view
|
||||
// for any state that render() doesn't manage.
|
||||
if (this.state.outgoing) {
|
||||
return (React.createElement(PendingConversationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
callState: this.state.callState,
|
||||
contact: this.state.contact,
|
||||
enableCancelButton: this._isCancellable()}
|
||||
));
|
||||
}
|
||||
|
||||
// For incoming calls that are in accepting state, display the
|
||||
// accept call view.
|
||||
if (this.state.callState === CALL_STATES.ALERTING) {
|
||||
return (React.createElement(AcceptCallView, {
|
||||
callType: this.state.callType,
|
||||
callerId: this.state.callerId,
|
||||
dispatcher: this.props.dispatcher}
|
||||
));
|
||||
}
|
||||
|
||||
// Otherwise we're still gathering or connecting, so
|
||||
// don't display anything.
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
switch (this.state.callState) {
|
||||
case CALL_STATES.CLOSE: {
|
||||
|
@ -993,12 +1021,7 @@ loop.conversationViews = (function(mozL10n) {
|
|||
return null;
|
||||
}
|
||||
default: {
|
||||
return (React.createElement(PendingConversationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
callState: this.state.callState,
|
||||
contact: this.state.contact,
|
||||
enableCancelButton: this._isCancellable()}
|
||||
));
|
||||
return this._renderViewFromCallType();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -144,14 +144,16 @@ loop.conversationViews = (function(mozL10n) {
|
|||
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
|
||||
|
||||
propTypes: {
|
||||
model: React.PropTypes.object.isRequired,
|
||||
video: React.PropTypes.bool.isRequired
|
||||
callType: React.PropTypes.string.isRequired,
|
||||
callerId: React.PropTypes.string.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
// Only for use by the ui-showcase
|
||||
showMenu: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showMenu: false,
|
||||
video: true
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -221,9 +223,8 @@ loop.conversationViews = (function(mozL10n) {
|
|||
|
||||
return (
|
||||
<div className="call-window">
|
||||
<CallIdentifierView video={this.props.video}
|
||||
peerIdentifier={this.props.model.getCallIdentifier()}
|
||||
urlCreationDate={this.props.model.get("urlCreationDate")}
|
||||
<CallIdentifierView video={this.props.callType === CALL_TYPES.AUDIO_VIDEO}
|
||||
peerIdentifier={this.props.callerId}
|
||||
showIcons={true} />
|
||||
|
||||
<div className="btn-group call-action-group">
|
||||
|
@ -964,6 +965,33 @@ loop.conversationViews = (function(mozL10n) {
|
|||
);
|
||||
},
|
||||
|
||||
_renderViewFromCallType: function() {
|
||||
// For outgoing calls we can display the pending conversation view
|
||||
// for any state that render() doesn't manage.
|
||||
if (this.state.outgoing) {
|
||||
return (<PendingConversationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
callState={this.state.callState}
|
||||
contact={this.state.contact}
|
||||
enableCancelButton={this._isCancellable()}
|
||||
/>);
|
||||
}
|
||||
|
||||
// For incoming calls that are in accepting state, display the
|
||||
// accept call view.
|
||||
if (this.state.callState === CALL_STATES.ALERTING) {
|
||||
return (<AcceptCallView
|
||||
callType={this.state.callType}
|
||||
callerId={this.state.callerId}
|
||||
dispatcher={this.props.dispatcher}
|
||||
/>);
|
||||
}
|
||||
|
||||
// Otherwise we're still gathering or connecting, so
|
||||
// don't display anything.
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
switch (this.state.callState) {
|
||||
case CALL_STATES.CLOSE: {
|
||||
|
@ -993,12 +1021,7 @@ loop.conversationViews = (function(mozL10n) {
|
|||
return null;
|
||||
}
|
||||
default: {
|
||||
return (<PendingConversationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
callState={this.state.callState}
|
||||
contact={this.state.contact}
|
||||
enableCancelButton={this._isCancellable()}
|
||||
/>);
|
||||
return this._renderViewFromCallType();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -233,17 +233,26 @@ loop.store = loop.store || {};
|
|||
]);
|
||||
|
||||
this.setStoreState({
|
||||
apiKey: actionData.apiKey,
|
||||
callerId: actionData.callerId,
|
||||
callId: actionData.callId,
|
||||
callState: CALL_STATES.GATHER,
|
||||
callType: actionData.callType,
|
||||
contact: actionData.contact,
|
||||
outgoing: windowType === "outgoing",
|
||||
windowId: actionData.windowId,
|
||||
callType: actionData.callType,
|
||||
callState: CALL_STATES.GATHER,
|
||||
videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
|
||||
progressURL: actionData.progressURL,
|
||||
sessionId: actionData.sessionId,
|
||||
sessionToken: actionData.sessionToken,
|
||||
videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY,
|
||||
websocketToken: actionData.websocketToken,
|
||||
windowId: actionData.windowId
|
||||
});
|
||||
|
||||
if (this.getStoreState("outgoing")) {
|
||||
this._setupOutgoingCall();
|
||||
} // XXX Else, other types aren't supported yet.
|
||||
} else {
|
||||
this._setupIncomingCall();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -292,15 +301,19 @@ loop.store = loop.store || {};
|
|||
},
|
||||
|
||||
/**
|
||||
* Cancels a call
|
||||
* Cancels a call. This can happen for incoming or outgoing calls.
|
||||
* Although the user doesn't "cancel" an incoming call, it may be that
|
||||
* the remote peer cancelled theirs before the incoming call was accepted.
|
||||
*/
|
||||
cancelCall: function() {
|
||||
var callState = this.getStoreState("callState");
|
||||
if (this._websocket &&
|
||||
(callState === CALL_STATES.CONNECTING ||
|
||||
callState === CALL_STATES.ALERTING)) {
|
||||
// Let the server know the user has hung up.
|
||||
this._websocket.cancel();
|
||||
if (this.getStoreState("outgoing")) {
|
||||
var callState = this.getStoreState("callState");
|
||||
if (this._websocket &&
|
||||
(callState === CALL_STATES.CONNECTING ||
|
||||
callState === CALL_STATES.ALERTING)) {
|
||||
// Let the server know the user has hung up.
|
||||
this._websocket.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
this._endSession();
|
||||
|
@ -369,6 +382,15 @@ loop.store = loop.store || {};
|
|||
this._endSession();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up an incoming call. All we really need to do here is
|
||||
* to connect the websocket, as we've already got all the information
|
||||
* when the window opened.
|
||||
*/
|
||||
_setupIncomingCall: function() {
|
||||
this._connectWebSocket();
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtains the outgoing call data from the server and handles the
|
||||
* result.
|
||||
|
@ -466,29 +488,54 @@ loop.store = loop.store || {};
|
|||
this.getStoreState("windowId"));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* For outgoing calls, we treat all terminations as failures.
|
||||
*
|
||||
* @param {Object} progressData The progress data received from the websocket.
|
||||
* @param {String} previousState The previous state the websocket was in.
|
||||
*/
|
||||
_handleWebSocketStateTerminated: function(progressData, previousState) {
|
||||
if (this.getStoreState("outgoing") ||
|
||||
(previousState !== WS_STATES.INIT &&
|
||||
previousState !== WS_STATES.ALERTING)) {
|
||||
// For outgoing calls we can treat everything as connection failure.
|
||||
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
|
||||
reason: progressData.reason
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatcher.dispatch(new sharedActions.CancelCall());
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to handle any progressed received from the websocket. This will
|
||||
* dispatch new actions so that the data can be handled appropriately.
|
||||
*
|
||||
* @param {Object} progressData The progress data received from the websocket.
|
||||
* @param {String} previousState The previous state the websocket was in.
|
||||
*/
|
||||
_handleWebSocketProgress: function(progressData) {
|
||||
var action;
|
||||
|
||||
_handleWebSocketProgress: function(progressData, previousState) {
|
||||
switch(progressData.state) {
|
||||
case WS_STATES.TERMINATED: {
|
||||
action = new sharedActions.ConnectionFailure({
|
||||
reason: progressData.reason
|
||||
});
|
||||
this._handleWebSocketStateTerminated(progressData, previousState);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
action = new sharedActions.ConnectionProgress({
|
||||
this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
|
||||
wsState: progressData.state
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatcher.dispatch(action);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -616,11 +616,12 @@ describe("loop.conversationViews", function () {
|
|||
loop.conversationViews.CallFailedView);
|
||||
});
|
||||
|
||||
it("should render the PendingConversationView when the call state is 'gather'",
|
||||
it("should render the PendingConversationView for outgoing calls when the call state is 'gather'",
|
||||
function() {
|
||||
store.setStoreState({
|
||||
callState: CALL_STATES.GATHER,
|
||||
contact: contact
|
||||
contact: contact,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
@ -629,6 +630,18 @@ describe("loop.conversationViews", function () {
|
|||
loop.conversationViews.PendingConversationView);
|
||||
});
|
||||
|
||||
it("should render the AcceptCallView for incoming calls when the call state is 'alerting'", function() {
|
||||
store.setStoreState({
|
||||
callState: CALL_STATES.ALERTING,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.AcceptCallView);
|
||||
});
|
||||
|
||||
it("should render the OngoingConversationView when the call state is 'ongoing'",
|
||||
function() {
|
||||
store.setStoreState({callState: CALL_STATES.ONGOING});
|
||||
|
@ -669,7 +682,8 @@ describe("loop.conversationViews", function () {
|
|||
function() {
|
||||
store.setStoreState({
|
||||
callState: CALL_STATES.GATHER,
|
||||
contact: contact
|
||||
contact: contact,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
|
|
@ -129,27 +129,20 @@ describe("loop.conversation", function() {
|
|||
});
|
||||
|
||||
describe("AppControllerView", function() {
|
||||
var conversationStore, conversation, client, ccView, oldTitle, dispatcher;
|
||||
var conversationStore, client, ccView, oldTitle, dispatcher;
|
||||
var conversationAppStore, roomStore;
|
||||
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.conversation.AppControllerView, {
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
roomStore: roomStore,
|
||||
sdk: {},
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop
|
||||
dispatcher: dispatcher
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
oldTitle = document.title;
|
||||
client = new loop.Client();
|
||||
conversation = new loop.shared.models.ConversationModel({}, {
|
||||
sdk: {}
|
||||
});
|
||||
dispatcher = new loop.Dispatcher();
|
||||
conversationStore = new loop.store.ConversationStore(
|
||||
dispatcher, {
|
||||
|
@ -195,20 +188,13 @@ describe("loop.conversation", function() {
|
|||
loop.conversationViews.CallControllerView);
|
||||
});
|
||||
|
||||
it("should display the IncomingConversationView for incoming calls", function() {
|
||||
sandbox.stub(conversation, "setIncomingSessionData");
|
||||
sandbox.stub(loop, "CallConnectionWebSocket").returns({
|
||||
promiseConnect: function() {
|
||||
return new Promise(function() {});
|
||||
},
|
||||
on: sandbox.spy()
|
||||
});
|
||||
it("should display the CallControllerView for incoming calls", function() {
|
||||
conversationAppStore.setStoreState({windowType: "incoming"});
|
||||
|
||||
ccView = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(ccView,
|
||||
loop.conversationViews.IncomingConversationView);
|
||||
loop.conversationViews.CallControllerView);
|
||||
});
|
||||
|
||||
it("should display the RoomView for rooms", function() {
|
||||
|
|
|
@ -299,6 +299,79 @@ describe("loop.store.ConversationStore", function () {
|
|||
.eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
|
||||
});
|
||||
|
||||
describe("incoming calls", function() {
|
||||
beforeEach(function() {
|
||||
store.setStoreState({outgoing: false});
|
||||
});
|
||||
|
||||
it("should initialize the websocket", function() {
|
||||
sandbox.stub(loop, "CallConnectionWebSocket").returns({
|
||||
promiseConnect: function() { return connectPromise; },
|
||||
on: sinon.spy()
|
||||
});
|
||||
|
||||
store.connectCall(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
|
||||
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
|
||||
url: "fakeURL",
|
||||
callId: "142536",
|
||||
websocketToken: "543216"
|
||||
});
|
||||
});
|
||||
|
||||
it("should connect the websocket to the server", function() {
|
||||
store.connectCall(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sinon.assert.calledOnce(store._websocket.promiseConnect);
|
||||
});
|
||||
|
||||
describe("WebSocket connection result", function() {
|
||||
beforeEach(function() {
|
||||
store.connectCall(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
it("should dispatch a connection progress action on success", function(done) {
|
||||
resolveConnectPromise(WS_STATES.INIT);
|
||||
|
||||
connectPromise.then(function() {
|
||||
checkFailures(done, function() {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionProgress"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("wsState", WS_STATES.INIT));
|
||||
});
|
||||
}, function() {
|
||||
done(new Error("Promise should have been resolve, not rejected"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch a connection failure action on failure", function(done) {
|
||||
rejectConnectPromise();
|
||||
|
||||
connectPromise.then(function() {
|
||||
done(new Error("Promise should have been rejected, not resolved"));
|
||||
}, function() {
|
||||
checkFailures(done, function() {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionFailure"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("reason", "websocket-setup"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("outgoing calls", function() {
|
||||
it("should request the outgoing call data", function() {
|
||||
dispatcher.dispatch(
|
||||
|
@ -632,7 +705,9 @@ describe("loop.store.ConversationStore", function () {
|
|||
sinon.assert.calledOnce(sdkDriver.disconnectSession);
|
||||
});
|
||||
|
||||
it("should send a cancel message to the websocket if it is open", function() {
|
||||
it("should send a cancel message to the websocket if it is open for outgoing calls", function() {
|
||||
store.setStoreState({outgoing: true});
|
||||
|
||||
store.cancelCall(new sharedActions.CancelCall());
|
||||
|
||||
sinon.assert.calledOnce(wsCancelSpy);
|
||||
|
@ -787,10 +862,14 @@ describe("loop.store.ConversationStore", function () {
|
|||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
it("should dispatch a connection failure action on 'terminate'", function() {
|
||||
it("should dispatch a connection failure action on 'terminate' for outgoing calls", function() {
|
||||
store.setStoreState({
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
store._websocket.trigger("progress", {
|
||||
state: WS_STATES.TERMINATED,
|
||||
reason: WEBSOCKET_REASONS.REJECT
|
||||
reason: WEBSOCKET_REASONS.REJECT,
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
|
@ -801,6 +880,56 @@ describe("loop.store.ConversationStore", function () {
|
|||
sinon.match.hasOwn("reason", WEBSOCKET_REASONS.REJECT));
|
||||
});
|
||||
|
||||
it("should dispatch a connection failure action on 'terminate' for incoming calls if the previous state was not 'alerting' or 'init'", function() {
|
||||
store.setStoreState({
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
store._websocket.trigger("progress", {
|
||||
state: WS_STATES.TERMINATED,
|
||||
reason: WEBSOCKET_REASONS.CANCEL
|
||||
}, WS_STATES.CONNECTING);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ConnectionFailure({
|
||||
reason: WEBSOCKET_REASONS.CANCEL
|
||||
}));
|
||||
});
|
||||
|
||||
it("should dispatch a cancel call action on 'terminate' for incoming calls if the previous state was 'init'", function() {
|
||||
store.setStoreState({
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
store._websocket.trigger("progress", {
|
||||
state: WS_STATES.TERMINATED,
|
||||
reason: WEBSOCKET_REASONS.CANCEL
|
||||
}, WS_STATES.INIT);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.CancelCall({}));
|
||||
});
|
||||
|
||||
it("should dispatch a cancel call action on 'terminate' for incoming calls if the previous state was 'alerting'", function() {
|
||||
store.setStoreState({
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
store._websocket.trigger("progress", {
|
||||
state: WS_STATES.TERMINATED,
|
||||
reason: WEBSOCKET_REASONS.CANCEL
|
||||
}, WS_STATES.ALERTING);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.CancelCall({}));
|
||||
});
|
||||
|
||||
it("should dispatch a connection progress action on 'alerting'", function() {
|
||||
store._websocket.trigger("progress", {state: WS_STATES.ALERTING});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче