gecko-dev/browser/components/loop/test/shared/conversationStore_test.js

769 lines
24 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.store.ConversationStore", function () {
"use strict";
var CALL_STATES = loop.store.CALL_STATES;
var WS_STATES = loop.store.WS_STATES;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
var contact;
var connectPromise, resolveConnectPromise, rejectConnectPromise;
var wsCancelSpy, wsCloseSpy, wsMediaUpSpy, fakeWebsocket;
function checkFailures(done, f) {
try {
f();
done();
} catch (err) {
done(err);
}
}
beforeEach(function() {
sandbox = sinon.sandbox.create();
contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
}]
};
navigator.mozLoop = {
getLoopPref: sandbox.stub(),
addConversationContext: sandbox.stub(),
calls: {
setCallInProgress: sandbox.stub(),
clearCallInProgress: sandbox.stub()
}
};
dispatcher = new loop.Dispatcher();
client = {
setupOutgoingCall: sinon.stub(),
requestCallUrl: sinon.stub()
};
sdkDriver = {
connectSession: sinon.stub(),
disconnectSession: sinon.stub()
};
wsCancelSpy = sinon.spy();
wsCloseSpy = sinon.spy();
wsMediaUpSpy = sinon.spy();
fakeWebsocket = {
cancel: wsCancelSpy,
close: wsCloseSpy,
mediaUp: wsMediaUpSpy
};
store = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
fakeSessionData = {
apiKey: "fakeKey",
callId: "142536",
sessionId: "321456",
sessionToken: "341256",
websocketToken: "543216",
windowId: "28",
progressURL: "fakeURL"
};
var dummySocket = {
close: sinon.spy(),
send: sinon.spy()
};
connectPromise = new Promise(function(resolve, reject) {
resolveConnectPromise = resolve;
rejectConnectPromise = reject;
});
sandbox.stub(loop.CallConnectionWebSocket.prototype,
"promiseConnect").returns(connectPromise);
});
afterEach(function() {
sandbox.restore();
});
describe("#initialize", function() {
it("should throw an error if the dispatcher is missing", function() {
expect(function() {
new loop.store.ConversationStore({}, {
client: client,
sdkDriver: sdkDriver
});
}).to.Throw(/dispatcher/);
});
it("should throw an error if the client is missing", function() {
expect(function() {
new loop.store.ConversationStore({}, {
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
}).to.Throw(/client/);
});
it("should throw an error if the sdkDriver is missing", function() {
expect(function() {
new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher
});
}).to.Throw(/sdkDriver/);
});
});
describe("#connectionFailure", function() {
beforeEach(function() {
store._websocket = fakeWebsocket;
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should ensure the websocket is closed", function() {
store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(wsCloseSpy);
});
it("should set the state to 'terminated'", function() {
store.set({callState: CALL_STATES.ALERTING});
store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
expect(store.get("callStateReason")).eql("fake");
});
it("should release mozLoop callsData", function() {
store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
describe("#connectionProgress", function() {
describe("progress: init", function() {
it("should change the state from 'gather' to 'connecting'", function() {
store.set({callState: CALL_STATES.GATHER});
store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.INIT}));
expect(store.get("callState")).eql(CALL_STATES.CONNECTING);
});
});
describe("progress: alerting", function() {
it("should change the state from 'gather' to 'alerting'", function() {
store.set({callState: CALL_STATES.GATHER});
store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.ALERTING}));
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
});
it("should change the state from 'init' to 'alerting'", function() {
store.set({callState: CALL_STATES.INIT});
store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.ALERTING}));
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
});
});
describe("progress: connecting", function() {
beforeEach(function() {
store.set({callState: CALL_STATES.ALERTING});
});
it("should change the state to 'ongoing'", function() {
store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
expect(store.get("callState")).eql(CALL_STATES.ONGOING);
});
it("should connect the session", function() {
store.set(fakeSessionData);
store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
sinon.assert.calledOnce(sdkDriver.connectSession);
sinon.assert.calledWithExactly(sdkDriver.connectSession, {
apiKey: "fakeKey",
sessionId: "321456",
sessionToken: "341256"
});
});
it("should call mozLoop.addConversationContext", function() {
store.set(fakeSessionData);
store.connectionProgress(
new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
sinon.assert.calledOnce(navigator.mozLoop.addConversationContext);
sinon.assert.calledWithExactly(navigator.mozLoop.addConversationContext,
"28", "321456", "142536");
});
});
});
describe("#setupWindowData", function() {
var fakeSetupWindowData;
beforeEach(function() {
store.set({callState: CALL_STATES.INIT});
fakeSetupWindowData = {
windowId: "123456",
type: "outgoing",
contact: contact,
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
};
});
it("should set the state to 'gather'", function() {
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("callState")).eql(CALL_STATES.GATHER);
});
it("should save the basic call information", function() {
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("windowId")).eql("123456");
expect(store.get("outgoing")).eql(true);
});
it("should save the basic information from the mozLoop api", function() {
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("contact")).eql(contact);
expect(store.get("callType")).eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
describe("outgoing calls", function() {
it("should request the outgoing call data", function() {
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
it("should include all email addresses in the call data", function() {
fakeSetupWindowData.contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
},
{
type: "work",
value: "emailFake",
pref: false
}]
};
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail", "emailFake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
it("should include trim phone numbers for the call data", function() {
fakeSetupWindowData.contact = {
name: [ "Mr Smith" ],
tel: [{
type: "home",
value: "+44-5667+345 496(2335)45+ 456+",
pref: true
}]
};
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["+445667345496233545456"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
it("should include all email and telephone values in the call data", function() {
fakeSetupWindowData.contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
}, {
type: "work",
value: "emailFake",
pref: false
}],
tel: [{
type: "work",
value: "01234567890",
pref: false
}, {
type: "home",
value: "09876543210",
pref: false
}]
};
dispatcher.dispatch(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail", "emailFake", "01234567890", "09876543210"],
sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
describe("server response handling", function() {
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connect call action on success", function() {
var callData = {
apiKey: "fakeKey"
};
client.setupOutgoingCall.callsArgWith(2, null, callData);
store.setupWindowData(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectCall"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("sessionData", callData));
});
it("should dispatch a connection failure action on failure", function() {
client.setupOutgoingCall.callsArgWith(2, {});
store.setupWindowData(
new sharedActions.SetupWindowData(fakeSetupWindowData));
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", "setup"));
});
});
});
});
describe("#connectCall", function() {
it("should save the call session data", function() {
store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
expect(store.get("apiKey")).eql("fakeKey");
expect(store.get("callId")).eql("142536");
expect(store.get("sessionId")).eql("321456");
expect(store.get("sessionToken")).eql("341256");
expect(store.get("websocketToken")).eql("543216");
expect(store.get("progressURL")).eql("fakeURL");
});
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("#hangupCall", function() {
var wsMediaFailSpy, wsCloseSpy;
beforeEach(function() {
wsMediaFailSpy = sinon.spy();
wsCloseSpy = sinon.spy();
store._websocket = {
mediaFail: wsMediaFailSpy,
close: wsCloseSpy
};
store.set({callState: CALL_STATES.ONGOING});
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should send a media-fail message to the websocket if it is open", function() {
store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(wsMediaFailSpy);
});
it("should ensure the websocket is closed", function() {
store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(wsCloseSpy);
});
it("should set the callState to finished", function() {
store.hangupCall(new sharedActions.HangupCall());
expect(store.get("callState")).eql(CALL_STATES.FINISHED);
});
it("should release mozLoop callsData", function() {
store.hangupCall(new sharedActions.HangupCall());
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
describe("#remotePeerDisconnected", function() {
var wsMediaFailSpy, wsCloseSpy;
beforeEach(function() {
wsMediaFailSpy = sinon.spy();
wsCloseSpy = sinon.spy();
store._websocket = {
mediaFail: wsMediaFailSpy,
close: wsCloseSpy
};
store.set({callState: CALL_STATES.ONGOING});
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should ensure the websocket is closed", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
sinon.assert.calledOnce(wsCloseSpy);
});
it("should release mozLoop callsData", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
it("should set the callState to finished if the peer hungup", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
expect(store.get("callState")).eql(CALL_STATES.FINISHED);
});
it("should set the callState to terminated if the peer was disconnected" +
"for an unintentional reason", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: false
}));
expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
});
it("should set the reason to peerNetworkDisconnected if the peer was" +
"disconnected for an unintentional reason", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: false
}));
expect(store.get("callStateReason")).eql("peerNetworkDisconnected");
});
});
describe("#cancelCall", function() {
beforeEach(function() {
store._websocket = fakeWebsocket;
store.set({callState: CALL_STATES.CONNECTING});
store.set({windowId: "42"});
});
it("should disconnect the session", function() {
store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
it("should send a cancel message to the websocket if it is open", function() {
store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(wsCancelSpy);
});
it("should ensure the websocket is closed", function() {
store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(wsCloseSpy);
});
it("should set the state to close if the call is connecting", function() {
store.cancelCall(new sharedActions.CancelCall());
expect(store.get("callState")).eql(CALL_STATES.CLOSE);
});
it("should set the state to close if the call has terminated already", function() {
store.set({callState: CALL_STATES.TERMINATED});
store.cancelCall(new sharedActions.CancelCall());
expect(store.get("callState")).eql(CALL_STATES.CLOSE);
});
it("should release mozLoop callsData", function() {
store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
describe("#retryCall", function() {
it("should set the state to gather", function() {
store.set({callState: CALL_STATES.TERMINATED});
store.retryCall(new sharedActions.RetryCall());
expect(store.get("callState")).eql(CALL_STATES.GATHER);
});
it("should request the outgoing call data", function() {
store.set({
callState: CALL_STATES.TERMINATED,
outgoing: true,
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
contact: contact
});
store.retryCall(new sharedActions.RetryCall());
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
});
describe("#mediaConnected", function() {
it("should send mediaUp via the websocket", function() {
store._websocket = fakeWebsocket;
store.mediaConnected(new sharedActions.MediaConnected());
sinon.assert.calledOnce(wsMediaUpSpy);
});
});
describe("#setMute", function() {
it("should save the mute state for the audio stream", function() {
store.set({"audioMuted": false});
dispatcher.dispatch(new sharedActions.SetMute({
type: "audio",
enabled: true
}));
expect(store.get("audioMuted")).eql(false);
});
it("should save the mute state for the video stream", function() {
store.set({"videoMuted": true});
dispatcher.dispatch(new sharedActions.SetMute({
type: "video",
enabled: false
}));
expect(store.get("videoMuted")).eql(true);
});
});
describe("#fetchEmailLink", function() {
it("should request a new call url to the server", function() {
store.fetchEmailLink(new sharedActions.FetchEmailLink());
sinon.assert.calledOnce(client.requestCallUrl);
sinon.assert.calledWith(client.requestCallUrl, "");
});
it("should update the emailLink attribute when the new call url is received",
function() {
client.requestCallUrl = function(callId, cb) {
cb(null, {callUrl: "http://fake.invalid/"});
};
store.fetchEmailLink(new sharedActions.FetchEmailLink());
expect(store.get("emailLink")).eql("http://fake.invalid/");
});
it("should trigger an error:emailLink event in case of failure",
function() {
var trigger = sandbox.stub(store, "trigger");
client.requestCallUrl = function(callId, cb) {
cb("error");
};
store.fetchEmailLink(new sharedActions.FetchEmailLink());
sinon.assert.calledOnce(trigger);
sinon.assert.calledWithExactly(trigger, "error:emailLink");
});
});
describe("Events", function() {
describe("Websocket progress", function() {
beforeEach(function() {
store.connectCall(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connection failure action on 'terminate'", function() {
store._websocket.trigger("progress", {
state: WS_STATES.TERMINATED,
reason: "reject"
});
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", "reject"));
});
it("should dispatch a connection progress action on 'alerting'", function() {
store._websocket.trigger("progress", {state: WS_STATES.ALERTING});
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.ALERTING));
});
});
});
});