signal-desktop/ts/test-electron/state/ducks/calling_test.ts

2185 lines
67 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2020-10-30 17:52:21 +00:00
import { assert } from 'chai';
import * as sinon from 'sinon';
import { cloneDeep, noop } from 'lodash';
import type { StateType as RootStateType } from '../../../state/reducer';
import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import type {
2020-11-13 19:57:55 +00:00
CallingStateType,
2020-11-20 17:19:28 +00:00
GroupCallStateChangeActionType,
} from '../../../state/ducks/calling';
import {
2020-11-13 19:57:55 +00:00
actions,
getActiveCall,
getEmptyState,
reducer,
} from '../../../state/ducks/calling';
2023-07-18 23:57:38 +00:00
import { isAnybodyElseInGroupCall } from '../../../state/ducks/callingHelpers';
2022-05-19 03:28:51 +00:00
import { truncateAudioLevel } from '../../../calling/truncateAudioLevel';
import { calling as callingService } from '../../../services/calling';
2020-11-13 19:57:55 +00:00
import {
CallMode,
CallState,
CallViewMode,
2020-11-13 19:57:55 +00:00
GroupCallConnectionState,
GroupCallJoinState,
} from '../../../types/Calling';
2021-10-26 22:59:08 +00:00
import { UUID } from '../../../types/UUID';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import type { UnwrapPromise } from '../../../types/Util';
2020-10-30 17:52:21 +00:00
describe('calling duck', () => {
2020-11-13 19:57:55 +00:00
const stateWithDirectCall: CallingStateType = {
...getEmptyState(),
callsByConversation: {
'fake-direct-call-conversation-id': {
callMode: CallMode.Direct as CallMode.Direct,
conversationId: 'fake-direct-call-conversation-id',
callState: CallState.Accepted,
isIncoming: false,
isVideoCall: false,
hasRemoteVideo: false,
},
},
};
const stateWithActiveDirectCall = {
...stateWithDirectCall,
activeCallState: {
conversationId: 'fake-direct-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: false,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
2020-11-17 15:07:53 +00:00
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
},
};
const stateWithIncomingDirectCall = {
...getEmptyState(),
callsByConversation: {
'fake-direct-call-conversation-id': {
callMode: CallMode.Direct as CallMode.Direct,
conversationId: 'fake-direct-call-conversation-id',
callState: CallState.Ringing,
isIncoming: true,
isVideoCall: false,
hasRemoteVideo: false,
},
},
};
2021-10-26 22:59:08 +00:00
const creatorUuid = UUID.generate().toString();
const differentCreatorUuid = UUID.generate().toString();
const remoteUuid = UUID.generate().toString();
const ringerUuid = UUID.generate().toString();
const stateWithGroupCall = {
2020-11-13 19:57:55 +00:00
...getEmptyState(),
callsByConversation: {
'fake-group-call-conversation-id': {
callMode: CallMode.Group as CallMode.Group,
2020-11-13 19:57:55 +00:00
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
2020-11-20 17:19:28 +00:00
peekInfo: {
2021-10-26 22:59:08 +00:00
uuids: [creatorUuid],
creatorUuid,
2020-11-20 17:19:28 +00:00
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 4 / 3,
},
],
},
},
};
2021-08-20 16:06:15 +00:00
const stateWithIncomingGroupCall = {
...stateWithGroupCall,
callsByConversation: {
...stateWithGroupCall.callsByConversation,
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
ringId: BigInt(123),
2021-10-26 22:59:08 +00:00
ringerUuid: UUID.generate().toString(),
2021-08-20 16:06:15 +00:00
},
},
};
const stateWithActiveGroupCall = {
2020-11-13 19:57:55 +00:00
...stateWithGroupCall,
activeCallState: {
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: false,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
2020-11-17 15:07:53 +00:00
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
2020-11-13 19:57:55 +00:00
pip: false,
settingsDialogOpen: false,
},
};
const stateWithActivePresentationViewGroupCall = {
...stateWithGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
viewMode: CallViewMode.Presentation,
},
};
const stateWithActiveSpeakerViewGroupCall = {
...stateWithGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
viewMode: CallViewMode.Speaker,
},
};
2022-07-08 20:46:25 +00:00
const ourACI = UUID.generate().toString();
2020-11-20 17:19:28 +00:00
const getEmptyRootState = () => {
const rootState = rootReducer(undefined, noopAction());
return {
...rootState,
user: {
...rootState.user,
2022-07-08 20:46:25 +00:00
ourACI,
2020-11-20 17:19:28 +00:00
},
};
};
beforeEach(function beforeEach() {
this.sandbox = sinon.createSandbox();
});
afterEach(function afterEach() {
this.sandbox.restore();
});
describe('actions', () => {
describe('getPresentingSources', () => {
beforeEach(function beforeEach() {
this.callingServiceGetPresentingSources = this.sandbox
.stub(callingService, 'getPresentingSources')
.resolves([
{
id: 'foo.bar',
name: 'Foo Bar',
thumbnail: 'xyz',
},
]);
});
it('retrieves sources from the calling service', async function test() {
const { getPresentingSources } = actions;
const dispatch = sinon.spy();
await getPresentingSources()(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(this.callingServiceGetPresentingSources);
});
it('dispatches SET_PRESENTING_SOURCES', async function test() {
const { getPresentingSources } = actions;
const dispatch = sinon.spy();
await getPresentingSources()(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/SET_PRESENTING_SOURCES',
payload: [
{
id: 'foo.bar',
name: 'Foo Bar',
thumbnail: 'xyz',
},
],
});
});
});
describe('remoteSharingScreenChange', () => {
it("updates whether someone's screen is being shared", () => {
const { remoteSharingScreenChange } = actions;
const payload = {
conversationId: 'fake-direct-call-conversation-id',
isSharingScreen: true,
};
const state = {
...stateWithActiveDirectCall,
};
const nextState = reducer(state, remoteSharingScreenChange(payload));
const expectedState = {
...stateWithActiveDirectCall,
callsByConversation: {
'fake-direct-call-conversation-id': {
...stateWithActiveDirectCall.callsByConversation[
'fake-direct-call-conversation-id'
],
isSharingScreen: true,
},
},
};
assert.deepEqual(nextState, expectedState);
});
});
describe('setPresenting', () => {
beforeEach(function beforeEach() {
this.callingServiceSetPresenting = this.sandbox.stub(
callingService,
'setPresenting'
);
});
it('calls setPresenting on the calling service', function test() {
const { setPresenting } = actions;
const dispatch = sinon.spy();
const presentedSource = {
id: 'window:786',
name: 'Application',
};
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting(presentedSource)(dispatch, getState, null);
sinon.assert.calledOnce(this.callingServiceSetPresenting);
sinon.assert.calledWith(
this.callingServiceSetPresenting,
'fake-group-call-conversation-id',
false,
presentedSource
);
});
it('dispatches SET_PRESENTING', () => {
const { setPresenting } = actions;
const dispatch = sinon.spy();
const presentedSource = {
id: 'window:786',
name: 'Application',
};
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting(presentedSource)(dispatch, getState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/SET_PRESENTING',
payload: presentedSource,
});
});
it('turns off presenting when no value is passed in', () => {
const dispatch = sinon.spy();
const { setPresenting } = actions;
const presentedSource = {
id: 'window:786',
name: 'Application',
};
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting(presentedSource)(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
const nextState = reducer(getState().calling, action);
assert.isDefined(nextState.activeCallState);
assert.equal(
nextState.activeCallState?.presentingSource,
presentedSource
);
assert.isUndefined(
nextState.activeCallState?.presentingSourcesAvailable
);
});
it('sets the presenting value when one is passed in', () => {
const dispatch = sinon.spy();
const { setPresenting } = actions;
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting()(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
const nextState = reducer(getState().calling, action);
assert.isDefined(nextState.activeCallState);
assert.isUndefined(nextState.activeCallState?.presentingSource);
assert.isUndefined(
nextState.activeCallState?.presentingSourcesAvailable
);
});
});
describe('acceptCall', () => {
const { acceptCall } = actions;
beforeEach(function beforeEach() {
this.callingServiceAccept = this.sandbox
2021-08-20 16:06:15 +00:00
.stub(callingService, 'acceptDirectCall')
.resolves();
this.callingServiceJoin = this.sandbox
.stub(callingService, 'joinGroupCall')
.resolves();
});
2021-08-20 16:06:15 +00:00
describe('accepting a direct call', () => {
const getState = () => ({
...getEmptyRootState(),
calling: stateWithIncomingDirectCall,
});
2021-08-20 16:06:15 +00:00
it('dispatches an ACCEPT_CALL_PENDING action', async () => {
const dispatch = sinon.spy();
2021-08-20 16:06:15 +00:00
await acceptCall({
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: true,
2021-08-20 16:06:15 +00:00
})(dispatch, getState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/ACCEPT_CALL_PENDING',
payload: {
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: true,
},
});
await acceptCall({
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: false,
})(dispatch, getState, null);
sinon.assert.calledTwice(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/ACCEPT_CALL_PENDING',
payload: {
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: false,
},
});
});
2021-08-20 16:06:15 +00:00
it('asks the calling service to accept the call', async function test() {
const dispatch = sinon.spy();
2021-08-20 16:06:15 +00:00
await acceptCall({
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: true,
})(dispatch, getState, null);
sinon.assert.calledOnce(this.callingServiceAccept);
sinon.assert.calledWith(
this.callingServiceAccept,
'fake-direct-call-conversation-id',
true
);
await acceptCall({
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: false,
2021-08-20 16:06:15 +00:00
})(dispatch, getState, null);
sinon.assert.calledTwice(this.callingServiceAccept);
sinon.assert.calledWith(
this.callingServiceAccept,
'fake-direct-call-conversation-id',
false
);
});
it('updates the active call state with ACCEPT_CALL_PENDING', async () => {
const dispatch = sinon.spy();
await acceptCall({
conversationId: 'fake-direct-call-conversation-id',
asVideoCall: true,
})(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
const result = reducer(stateWithIncomingDirectCall, action);
assert.deepEqual(result.activeCallState, {
conversationId: 'fake-direct-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
2021-08-20 16:06:15 +00:00
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
2021-08-20 16:06:15 +00:00
pip: false,
settingsDialogOpen: false,
});
});
});
2021-08-20 16:06:15 +00:00
describe('accepting a group call', () => {
const getState = () => ({
...getEmptyRootState(),
calling: stateWithIncomingGroupCall,
});
2021-08-20 16:06:15 +00:00
it('dispatches an ACCEPT_CALL_PENDING action', async () => {
const dispatch = sinon.spy();
2021-08-20 16:06:15 +00:00
await acceptCall({
conversationId: 'fake-group-call-conversation-id',
asVideoCall: true,
})(dispatch, getState, null);
2021-08-20 16:06:15 +00:00
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/ACCEPT_CALL_PENDING',
payload: {
conversationId: 'fake-group-call-conversation-id',
asVideoCall: true,
},
});
2021-08-20 16:06:15 +00:00
await acceptCall({
conversationId: 'fake-group-call-conversation-id',
asVideoCall: false,
})(dispatch, getState, null);
2021-08-20 16:06:15 +00:00
sinon.assert.calledTwice(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/ACCEPT_CALL_PENDING',
payload: {
conversationId: 'fake-group-call-conversation-id',
asVideoCall: false,
},
});
});
2021-08-20 16:06:15 +00:00
it('asks the calling service to join the call', async function test() {
const dispatch = sinon.spy();
2021-08-20 16:06:15 +00:00
await acceptCall({
conversationId: 'fake-group-call-conversation-id',
asVideoCall: true,
})(dispatch, getState, null);
sinon.assert.calledOnce(this.callingServiceJoin);
sinon.assert.calledWith(
this.callingServiceJoin,
'fake-group-call-conversation-id',
true,
true
);
await acceptCall({
conversationId: 'fake-group-call-conversation-id',
asVideoCall: false,
})(dispatch, getState, null);
sinon.assert.calledTwice(this.callingServiceJoin);
sinon.assert.calledWith(
this.callingServiceJoin,
'fake-group-call-conversation-id',
true,
false
);
});
it('updates the active call state with ACCEPT_CALL_PENDING', async () => {
const dispatch = sinon.spy();
await acceptCall({
conversationId: 'fake-group-call-conversation-id',
asVideoCall: true,
})(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
const result = reducer(stateWithIncomingGroupCall, action);
assert.deepEqual(result.activeCallState, {
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
2021-08-20 16:06:15 +00:00
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
2021-08-20 16:06:15 +00:00
pip: false,
settingsDialogOpen: false,
});
});
});
});
2020-11-13 19:57:55 +00:00
describe('cancelCall', () => {
const { cancelCall } = actions;
beforeEach(function beforeEach() {
this.callingServiceStopCallingLobby = this.sandbox.stub(
callingService,
'stopCallingLobby'
);
});
it('stops the calling lobby for that conversation', function test() {
cancelCall({ conversationId: '123' });
sinon.assert.calledOnce(this.callingServiceStopCallingLobby);
sinon.assert.calledWith(this.callingServiceStopCallingLobby, '123');
});
it('completely removes an active direct call from the state', () => {
const result = reducer(
stateWithActiveDirectCall,
cancelCall({ conversationId: 'fake-direct-call-conversation-id' })
);
assert.notProperty(
result.callsByConversation,
'fake-direct-call-conversation-id'
);
assert.isUndefined(result.activeCallState);
});
it('removes the active group call, but leaves it in the state', () => {
const result = reducer(
stateWithActiveGroupCall,
cancelCall({ conversationId: 'fake-group-call-conversation-id' })
);
assert.property(
result.callsByConversation,
'fake-group-call-conversation-id'
);
assert.isUndefined(result.activeCallState);
});
});
2021-08-20 16:06:15 +00:00
describe('cancelIncomingGroupCallRing', () => {
const { cancelIncomingGroupCallRing } = actions;
it('does nothing if there is no associated group call', () => {
const state = getEmptyState();
const action = cancelIncomingGroupCallRing({
conversationId: 'garbage',
ringId: BigInt(1),
});
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it("does nothing if the ring to cancel isn't the same one", () => {
const action = cancelIncomingGroupCallRing({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(999),
});
const result = reducer(stateWithIncomingGroupCall, action);
assert.strictEqual(result, stateWithIncomingGroupCall);
});
it('removes the ring state, but not the call', () => {
2021-08-20 16:06:15 +00:00
const action = cancelIncomingGroupCallRing({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(123),
});
const result = reducer(stateWithIncomingGroupCall, action);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
// It'd be nice to do this with an assert, but Chai doesn't understand it.
if (call?.callMode !== CallMode.Group) {
throw new Error('Expected to find a group call');
}
assert.isUndefined(call.ringId);
assert.isUndefined(call.ringerUuid);
});
});
describe('declineCall', () => {
const { declineCall } = actions;
let declineDirectCall: sinon.SinonStub;
let declineGroupCall: sinon.SinonStub;
beforeEach(function beforeEach() {
declineDirectCall = this.sandbox.stub(
callingService,
'declineDirectCall'
);
declineGroupCall = this.sandbox.stub(
callingService,
'declineGroupCall'
);
});
describe('declining a direct call', () => {
const getState = () => ({
...getEmptyRootState(),
calling: stateWithIncomingDirectCall,
});
it('dispatches a DECLINE_DIRECT_CALL action', () => {
const dispatch = sinon.spy();
declineCall({ conversationId: 'fake-direct-call-conversation-id' })(
dispatch,
getState,
null
);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/DECLINE_DIRECT_CALL',
payload: {
conversationId: 'fake-direct-call-conversation-id',
},
});
});
it('asks the calling service to decline the call', () => {
const dispatch = sinon.spy();
declineCall({ conversationId: 'fake-direct-call-conversation-id' })(
dispatch,
getState,
null
);
sinon.assert.calledOnce(declineDirectCall);
sinon.assert.calledWith(
declineDirectCall,
'fake-direct-call-conversation-id'
);
});
it('removes the call from the state', () => {
const dispatch = sinon.spy();
declineCall({ conversationId: 'fake-direct-call-conversation-id' })(
dispatch,
getState,
null
);
const action = dispatch.getCall(0).args[0];
const result = reducer(stateWithIncomingGroupCall, action);
assert.notProperty(
result.callsByConversation,
'fake-direct-call-conversation-id'
);
});
});
describe('declining a group call', () => {
const getState = () => ({
...getEmptyRootState(),
calling: stateWithIncomingGroupCall,
});
it('dispatches a CANCEL_INCOMING_GROUP_CALL_RING action', () => {
const dispatch = sinon.spy();
declineCall({ conversationId: 'fake-group-call-conversation-id' })(
dispatch,
getState,
null
);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/CANCEL_INCOMING_GROUP_CALL_RING',
payload: {
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(123),
},
});
});
it('asks the calling service to decline the call', () => {
const dispatch = sinon.spy();
declineCall({ conversationId: 'fake-group-call-conversation-id' })(
dispatch,
getState,
null
);
sinon.assert.calledOnce(declineGroupCall);
sinon.assert.calledWith(
declineGroupCall,
'fake-group-call-conversation-id',
BigInt(123)
);
});
// NOTE: The state effects of this action are tested with
// `cancelIncomingGroupCallRing`.
});
});
describe('groupCallAudioLevelsChange', () => {
const { groupCallAudioLevelsChange } = actions;
const remoteDeviceStates = [
{ audioLevel: 0.3, demuxId: 1 },
{ audioLevel: 0.4, demuxId: 2 },
{ audioLevel: 0.5, demuxId: 3 },
{ audioLevel: 0.2, demuxId: 7 },
{ audioLevel: 0.1, demuxId: 8 },
{ audioLevel: 0, demuxId: 9 },
];
2022-05-19 03:28:51 +00:00
const remoteAudioLevels = new Map<number, number>([
[1, truncateAudioLevel(0.3)],
[2, truncateAudioLevel(0.4)],
[3, truncateAudioLevel(0.5)],
[7, truncateAudioLevel(0.2)],
[8, truncateAudioLevel(0.1)],
]);
it("does nothing if there's no relevant call", () => {
const action = groupCallAudioLevelsChange({
conversationId: 'garbage',
localAudioLevel: 1,
remoteDeviceStates,
});
const result = reducer(stateWithActiveGroupCall, action);
assert.strictEqual(result, stateWithActiveGroupCall);
});
it('does nothing if the state change would be a no-op', () => {
const state = {
...stateWithActiveGroupCall,
callsByConversation: {
'fake-group-call-conversation-id': {
...stateWithActiveGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
2022-05-19 03:28:51 +00:00
remoteAudioLevels,
},
},
};
const action = groupCallAudioLevelsChange({
conversationId: 'fake-group-call-conversation-id',
2022-05-19 03:28:51 +00:00
localAudioLevel: 0.001,
remoteDeviceStates,
});
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('updates the set of speaking participants, including yourself', () => {
const action = groupCallAudioLevelsChange({
conversationId: 'fake-group-call-conversation-id',
localAudioLevel: 0.8,
remoteDeviceStates,
});
const result = reducer(stateWithActiveGroupCall, action);
2022-05-19 03:28:51 +00:00
assert.strictEqual(
result.activeCallState?.localAudioLevel,
truncateAudioLevel(0.8)
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
if (call?.callMode !== CallMode.Group) {
throw new Error('Expected a group call to be found');
}
2022-05-19 03:28:51 +00:00
assert.deepStrictEqual(call.remoteAudioLevels, remoteAudioLevels);
});
});
2020-11-13 19:57:55 +00:00
describe('groupCallStateChange', () => {
const { groupCallStateChange } = actions;
2020-11-20 17:19:28 +00:00
function getAction(
...args: Parameters<typeof groupCallStateChange>
): GroupCallStateChangeActionType {
const dispatch = sinon.spy();
groupCallStateChange(...args)(dispatch, getEmptyRootState, null);
return dispatch.getCall(0).args[0];
}
2020-11-13 19:57:55 +00:00
it('saves a new call to the map of conversations', () => {
const result = reducer(
getEmptyState(),
2020-11-20 17:19:28 +00:00
getAction({
2020-11-13 19:57:55 +00:00
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joining,
hasLocalAudio: true,
hasLocalVideo: false,
2020-11-20 17:19:28 +00:00
peekInfo: {
2021-10-26 22:59:08 +00:00
uuids: [creatorUuid],
creatorUuid,
2020-11-20 17:19:28 +00:00
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 4 / 3,
},
],
})
);
assert.deepEqual(
result.callsByConversation['fake-group-call-conversation-id'],
{
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joining,
2020-11-20 17:19:28 +00:00
peekInfo: {
2021-10-26 22:59:08 +00:00
uuids: [creatorUuid],
creatorUuid,
2020-11-20 17:19:28 +00:00
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 4 / 3,
},
],
}
);
});
it('updates a call in the map of conversations', () => {
const result = reducer(
stateWithGroupCall,
2020-11-20 17:19:28 +00:00
getAction({
2020-11-13 19:57:55 +00:00
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: false,
2020-11-20 17:19:28 +00:00
peekInfo: {
2020-12-02 18:14:03 +00:00
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
2020-11-20 17:19:28 +00:00
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 16 / 9,
},
],
})
);
assert.deepEqual(
result.callsByConversation['fake-group-call-conversation-id'],
{
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
2020-11-20 17:19:28 +00:00
peekInfo: {
2020-12-02 18:14:03 +00:00
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
2020-11-20 17:19:28 +00:00
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 16 / 9,
},
],
}
);
});
2021-08-20 16:06:15 +00:00
it("keeps the existing ring state if you haven't joined the call", () => {
const state = {
...stateWithGroupCall,
callsByConversation: {
...stateWithGroupCall.callsByConversation,
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
},
},
};
const result = reducer(
state,
getAction({
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
hasLocalAudio: true,
hasLocalVideo: false,
peekInfo: {
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2021-08-20 16:06:15 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
})
);
assert.include(
result.callsByConversation['fake-group-call-conversation-id'],
{
callMode: CallMode.Group,
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
}
);
});
it("removes the ring state if you've joined the call", () => {
const state = {
...stateWithGroupCall,
callsByConversation: {
...stateWithGroupCall.callsByConversation,
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
},
},
};
const result = reducer(
state,
getAction({
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: false,
peekInfo: {
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2021-08-20 16:06:15 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
})
);
assert.notProperty(
result.callsByConversation['fake-group-call-conversation-id'],
'ringId'
);
assert.notProperty(
result.callsByConversation['fake-group-call-conversation-id'],
'ringerUuid'
);
});
2020-11-13 19:57:55 +00:00
it("if no call is active, doesn't touch the active call state", () => {
const result = reducer(
stateWithGroupCall,
2020-11-20 17:19:28 +00:00
getAction({
2020-11-13 19:57:55 +00:00
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: false,
2020-11-20 17:19:28 +00:00
peekInfo: {
2020-12-02 18:14:03 +00:00
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
2020-11-20 17:19:28 +00:00
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 16 / 9,
},
],
})
);
assert.isUndefined(result.activeCallState);
});
it("if the call is not active, doesn't touch the active call state", () => {
const result = reducer(
stateWithActiveGroupCall,
2020-11-20 17:19:28 +00:00
getAction({
2020-11-13 19:57:55 +00:00
conversationId: 'another-fake-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: true,
2020-11-20 17:19:28 +00:00
peekInfo: {
2020-12-02 18:14:03 +00:00
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
2020-11-20 17:19:28 +00:00
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 16 / 9,
},
],
})
);
assert.deepEqual(result.activeCallState, {
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: false,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
2020-11-17 15:07:53 +00:00
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
2020-11-13 19:57:55 +00:00
pip: false,
settingsDialogOpen: false,
});
});
it('if the call is active, updates the active call state', () => {
const result = reducer(
stateWithActiveGroupCall,
2020-11-20 17:19:28 +00:00
getAction({
2020-11-13 19:57:55 +00:00
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: true,
2020-11-20 17:19:28 +00:00
peekInfo: {
2020-12-02 18:14:03 +00:00
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
2020-11-20 17:19:28 +00:00
maxDevices: 16,
deviceCount: 1,
},
2020-11-13 19:57:55 +00:00
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-11-13 19:57:55 +00:00
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-11-13 19:57:55 +00:00
videoAspectRatio: 16 / 9,
},
],
})
);
assert.strictEqual(
result.activeCallState?.conversationId,
'fake-group-call-conversation-id'
);
assert.isTrue(result.activeCallState?.hasLocalAudio);
assert.isTrue(result.activeCallState?.hasLocalVideo);
});
it("doesn't stop ringing if nobody is in the call", () => {
const state = {
...stateWithActiveGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
outgoingRing: true,
},
};
const result = reducer(
state,
getAction({
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: true,
peekInfo: {
uuids: [],
maxDevices: 16,
deviceCount: 0,
},
remoteParticipants: [],
})
);
assert.isTrue(result.activeCallState?.outgoingRing);
});
it('stops ringing if someone enters the call', () => {
const state = {
...stateWithActiveGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
outgoingRing: true,
},
};
const result = reducer(
state,
getAction({
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: true,
peekInfo: {
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [],
})
);
assert.isFalse(result.activeCallState?.outgoingRing);
});
2020-11-13 19:57:55 +00:00
});
2020-11-20 17:19:28 +00:00
describe('peekNotConnectedGroupCall', () => {
const { peekNotConnectedGroupCall } = actions;
beforeEach(function beforeEach() {
this.callingServicePeekGroupCall = this.sandbox.stub(
callingService,
'peekGroupCall'
);
this.callingServiceUpdateCallHistoryForGroupCall = this.sandbox.stub(
callingService,
'updateCallHistoryForGroupCall'
);
2020-11-20 17:19:28 +00:00
this.clock = this.sandbox.useFakeTimers();
});
describe('thunk', () => {
function noopTest(connectionState: GroupCallConnectionState) {
2021-12-09 08:06:04 +00:00
return async function test(this: Mocha.Context) {
2020-11-20 17:19:28 +00:00
const dispatch = sinon.spy();
await peekNotConnectedGroupCall({
conversationId: 'fake-group-call-conversation-id',
})(
dispatch,
() => ({
...getEmptyRootState(),
calling: {
...stateWithGroupCall,
callsByConversation: {
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
connectionState,
},
},
},
}),
null
);
sinon.assert.notCalled(dispatch);
sinon.assert.notCalled(this.callingServicePeekGroupCall);
};
}
it(
'no-ops if trying to peek at a connecting group call',
noopTest(GroupCallConnectionState.Connecting)
);
it(
'no-ops if trying to peek at a connected group call',
noopTest(GroupCallConnectionState.Connected)
);
it(
'no-ops if trying to peek at a reconnecting group call',
noopTest(GroupCallConnectionState.Reconnecting)
);
// These tests are incomplete.
});
});
describe('returnToActiveCall', () => {
const { returnToActiveCall } = actions;
it('does nothing if not in PiP mode', () => {
const result = reducer(stateWithActiveDirectCall, returnToActiveCall());
assert.deepEqual(result, stateWithActiveDirectCall);
});
it('closes the PiP', () => {
const state = {
...stateWithActiveDirectCall,
activeCallState: {
...stateWithActiveDirectCall.activeCallState,
pip: true,
},
};
const result = reducer(state, returnToActiveCall());
assert.deepEqual(result, stateWithActiveDirectCall);
});
});
2021-08-20 16:06:15 +00:00
describe('receiveIncomingGroupCall', () => {
const { receiveIncomingGroupCall } = actions;
it('does nothing if the call was already ringing', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
});
const result = reducer(stateWithIncomingGroupCall, action);
assert.strictEqual(result, stateWithIncomingGroupCall);
});
it('does nothing if the call was already joined', () => {
const state = {
...stateWithGroupCall,
callsByConversation: {
...stateWithGroupCall.callsByConversation,
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
joinState: GroupCallJoinState.Joined,
},
},
};
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
});
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('creates a new group call if one did not exist', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
});
const result = reducer(getEmptyState(), action);
assert.deepEqual(
result.callsByConversation['fake-group-call-conversation-id'],
{
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.NotConnected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
},
remoteParticipants: [],
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
}
);
});
it('attaches ring state to an existing call', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
});
const result = reducer(stateWithGroupCall, action);
assert.include(
result.callsByConversation['fake-group-call-conversation-id'],
{
ringId: BigInt(456),
2021-10-26 22:59:08 +00:00
ringerUuid,
2021-08-20 16:06:15 +00:00
}
);
});
});
describe('setLocalAudio', () => {
const { setLocalAudio } = actions;
2020-10-30 17:52:21 +00:00
beforeEach(function beforeEach() {
this.callingServiceSetOutgoingAudio = this.sandbox.stub(
callingService,
'setOutgoingAudio'
);
2020-10-30 17:52:21 +00:00
});
it('dispatches a SET_LOCAL_AUDIO_FULFILLED action', () => {
const dispatch = sinon.spy();
2020-11-13 19:57:55 +00:00
setLocalAudio({ enabled: true })(
dispatch,
() => ({
...getEmptyRootState(),
calling: stateWithActiveDirectCall,
}),
null
);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/SET_LOCAL_AUDIO_FULFILLED',
payload: { enabled: true },
});
});
it('updates the outgoing audio for the active call', function test() {
const dispatch = sinon.spy();
setLocalAudio({ enabled: false })(
dispatch,
() => ({
...getEmptyRootState(),
calling: stateWithActiveDirectCall,
}),
null
);
sinon.assert.calledOnce(this.callingServiceSetOutgoingAudio);
sinon.assert.calledWith(
this.callingServiceSetOutgoingAudio,
'fake-direct-call-conversation-id',
false
);
setLocalAudio({ enabled: true })(
dispatch,
() => ({
...getEmptyRootState(),
calling: stateWithActiveDirectCall,
}),
null
);
sinon.assert.calledTwice(this.callingServiceSetOutgoingAudio);
sinon.assert.calledWith(
this.callingServiceSetOutgoingAudio,
'fake-direct-call-conversation-id',
true
2020-10-30 17:52:21 +00:00
);
});
it('updates the local audio state with SET_LOCAL_AUDIO_FULFILLED', () => {
const dispatch = sinon.spy();
2020-11-13 19:57:55 +00:00
setLocalAudio({ enabled: false })(
dispatch,
() => ({
...getEmptyRootState(),
calling: stateWithActiveDirectCall,
}),
null
);
const action = dispatch.getCall(0).args[0];
const result = reducer(stateWithActiveDirectCall, action);
assert.isFalse(result.activeCallState?.hasLocalAudio);
});
});
describe('setOutgoingRing', () => {
const { setOutgoingRing } = actions;
it('enables a desire to ring', () => {
const action = setOutgoingRing(true);
const result = reducer(stateWithActiveGroupCall, action);
assert.isTrue(result.activeCallState?.outgoingRing);
});
it('disables a desire to ring', () => {
const action = setOutgoingRing(false);
const result = reducer(stateWithActiveDirectCall, action);
assert.isFalse(result.activeCallState?.outgoingRing);
});
});
describe('startCallingLobby', () => {
const { startCallingLobby } = actions;
let rootState: RootStateType;
let startCallingLobbyStub: sinon.SinonStub;
beforeEach(function beforeEach() {
startCallingLobbyStub = this.sandbox
.stub(callingService, 'startCallingLobby')
.resolves();
const emptyRootState = getEmptyRootState();
rootState = {
...emptyRootState,
conversations: {
...emptyRootState.conversations,
conversationLookup: {
'fake-conversation-id': getDefaultConversation(),
},
},
};
});
describe('thunk', () => {
it('asks the calling service to start the lobby', async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(noop, () => rootState, null);
sinon.assert.calledOnce(startCallingLobbyStub);
});
it('requests audio by default', async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(noop, () => rootState, null);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
2020-11-13 19:57:55 +00:00
hasLocalAudio: true,
});
});
it("doesn't request audio if the group call already has 8 devices", async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(
noop,
() => {
const callingState = cloneDeep(stateWithGroupCall);
callingState.callsByConversation[
'fake-group-call-conversation-id'
].peekInfo.deviceCount = 8;
return { ...rootState, calling: callingState };
},
null
);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
2020-11-13 19:57:55 +00:00
hasLocalVideo: true,
});
});
it('requests video when starting a video call', async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(noop, () => rootState, null);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
hasLocalVideo: true,
});
});
it("doesn't request video when not a video call", async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: false,
})(noop, () => rootState, null);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
hasLocalVideo: false,
});
});
it('dispatches an action if the calling lobby returns something', async () => {
startCallingLobbyStub.resolves({
callMode: CallMode.Direct,
hasLocalAudio: true,
hasLocalVideo: true,
});
const dispatch = sinon.stub();
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(dispatch, () => rootState, null);
sinon.assert.calledOnce(dispatch);
});
it("doesn't dispatch an action if the calling lobby returns nothing", async () => {
const dispatch = sinon.stub();
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(dispatch, () => rootState, null);
sinon.assert.notCalled(dispatch);
});
2020-10-30 17:52:21 +00:00
});
2020-12-02 18:14:03 +00:00
describe('action', () => {
const getState = async (
callingState: CallingStateType,
callingServiceResult: UnwrapPromise<
ReturnType<typeof callingService.startCallingLobby>
>,
conversationId = 'fake-conversation-id'
): Promise<CallingStateType> => {
startCallingLobbyStub.resolves(callingServiceResult);
const dispatch = sinon.stub();
await startCallingLobby({
conversationId,
isVideoCall: true,
})(dispatch, () => ({ ...rootState, calling: callingState }), null);
const action = dispatch.getCall(0).args[0];
return reducer(callingState, action);
};
it('saves a direct call and makes it active', async () => {
const result = await getState(getEmptyState(), {
callMode: CallMode.Direct as const,
hasLocalAudio: true,
hasLocalVideo: true,
});
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
isIncoming: false,
isVideoCall: true,
});
assert.deepEqual(result.activeCallState, {
2020-12-02 18:14:03 +00:00
conversationId: 'fake-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
outgoingRing: true,
});
});
it('saves a group call and makes it active', async () => {
const result = await getState(getEmptyState(), {
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
2020-12-02 18:14:03 +00:00
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
2021-10-26 22:59:08 +00:00
uuids: [creatorUuid],
creatorUuid,
2020-12-02 18:14:03 +00:00
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-12-02 18:14:03 +00:00
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-12-02 18:14:03 +00:00
videoAspectRatio: 4 / 3,
},
],
});
2020-12-02 18:14:03 +00:00
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Group,
conversationId: 'fake-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
2020-12-02 18:14:03 +00:00
},
remoteParticipants: [
{
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
});
assert.deepEqual(
result.activeCallState?.conversationId,
'fake-conversation-id'
);
assert.isFalse(result.activeCallState?.outgoingRing);
2020-12-02 18:14:03 +00:00
});
it('chooses fallback peek info if none is sent and there is no existing call', async () => {
const result = await getState(getEmptyState(), {
2020-12-02 18:14:03 +00:00
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [],
});
2020-12-02 18:14:03 +00:00
const call = result.callsByConversation['fake-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
});
2020-12-02 18:14:03 +00:00
});
it("doesn't overwrite an existing group call's peek info if none was sent", async () => {
const result = await getState(stateWithGroupCall, {
2020-12-02 18:14:03 +00:00
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-12-02 18:14:03 +00:00
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-12-02 18:14:03 +00:00
videoAspectRatio: 4 / 3,
},
],
});
2020-12-02 18:14:03 +00:00
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
});
2020-12-02 18:14:03 +00:00
});
it("can overwrite an existing group call's peek info", async () => {
const state = {
...getEmptyState(),
callsByConversation: {
'fake-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
conversationId: 'fake-conversation-id',
},
},
};
const result = await getState(state, {
2020-12-02 18:14:03 +00:00
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
2021-10-26 22:59:08 +00:00
uuids: [differentCreatorUuid],
creatorUuid: differentCreatorUuid,
2020-12-02 18:14:03 +00:00
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
},
remoteParticipants: [
{
2021-10-26 22:59:08 +00:00
uuid: remoteUuid,
2020-12-02 18:14:03 +00:00
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
2020-12-02 18:14:03 +00:00
videoAspectRatio: 4 / 3,
},
],
});
2020-12-02 18:14:03 +00:00
const call = result.callsByConversation['fake-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [differentCreatorUuid],
creatorUuid: differentCreatorUuid,
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
});
2020-12-02 18:14:03 +00:00
});
2021-08-20 16:06:15 +00:00
it("doesn't overwrite an existing group call's ring state if it was set previously", async () => {
const result = await getState(
{
...stateWithGroupCall,
callsByConversation: {
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
ringId: BigInt(987),
ringerUuid,
},
2021-08-20 16:06:15 +00:00
},
},
{
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [
{
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
}
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
// It'd be nice to do this with an assert, but Chai doesn't understand it.
if (call?.callMode !== CallMode.Group) {
throw new Error('Expected to find a group call');
}
2021-08-20 16:06:15 +00:00
assert.strictEqual(call.ringId, BigInt(987));
assert.strictEqual(call.ringerUuid, ringerUuid);
});
2021-08-20 16:06:15 +00:00
});
});
2020-10-30 17:52:21 +00:00
describe('startCall', () => {
const { startCall } = actions;
beforeEach(function beforeEach() {
2020-11-13 19:57:55 +00:00
this.callingStartOutgoingDirectCall = this.sandbox.stub(
callingService,
2020-11-13 19:57:55 +00:00
'startOutgoingDirectCall'
);
this.callingJoinGroupCall = this.sandbox
.stub(callingService, 'joinGroupCall')
.resolves();
2020-10-30 17:52:21 +00:00
});
it('asks the calling service to start an outgoing direct call', async function test() {
2020-11-13 19:57:55 +00:00
const dispatch = sinon.spy();
await startCall({
2020-11-13 19:57:55 +00:00
callMode: CallMode.Direct,
conversationId: '123',
hasLocalAudio: true,
hasLocalVideo: false,
2020-11-13 19:57:55 +00:00
})(dispatch, getEmptyRootState, null);
2020-11-13 19:57:55 +00:00
sinon.assert.calledOnce(this.callingStartOutgoingDirectCall);
sinon.assert.calledWith(
2020-11-13 19:57:55 +00:00
this.callingStartOutgoingDirectCall,
'123',
true,
false
2020-10-30 17:52:21 +00:00
);
2020-11-13 19:57:55 +00:00
sinon.assert.notCalled(this.callingJoinGroupCall);
2020-10-30 17:52:21 +00:00
});
it('asks the calling service to join a group call', async function test() {
2020-11-13 19:57:55 +00:00
const dispatch = sinon.spy();
await startCall({
2020-11-13 19:57:55 +00:00
callMode: CallMode.Group,
conversationId: '123',
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(this.callingJoinGroupCall);
sinon.assert.calledWith(this.callingJoinGroupCall, '123', true, false);
sinon.assert.notCalled(this.callingStartOutgoingDirectCall);
});
it('saves direct calls and makes them active', async () => {
2020-11-13 19:57:55 +00:00
const dispatch = sinon.spy();
await startCall({
2020-11-13 19:57:55 +00:00
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
const action = dispatch.getCall(0).args[0];
const result = reducer(getEmptyState(), action);
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
2020-11-13 19:57:55 +00:00
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
callState: CallState.Prering,
isIncoming: false,
isVideoCall: false,
});
assert.deepEqual(result.activeCallState, {
conversationId: 'fake-conversation-id',
hasLocalAudio: true,
hasLocalVideo: false,
2022-05-19 03:28:51 +00:00
localAudioLevel: 0,
viewMode: CallViewMode.Grid,
2020-11-17 15:07:53 +00:00
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
outgoingRing: true,
});
2020-10-30 17:52:21 +00:00
});
2020-11-13 19:57:55 +00:00
2023-02-24 23:18:57 +00:00
it("doesn't dispatch any actions for group calls", async () => {
2020-11-13 19:57:55 +00:00
const dispatch = sinon.spy();
2023-02-24 23:18:57 +00:00
await startCall({
2020-11-13 19:57:55 +00:00
callMode: CallMode.Group,
conversationId: '123',
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
sinon.assert.notCalled(dispatch);
});
});
2020-10-30 17:52:21 +00:00
describe('toggleSettings', () => {
const { toggleSettings } = actions;
it('toggles the settings dialog', () => {
const afterOneToggle = reducer(
stateWithActiveDirectCall,
toggleSettings()
2020-10-30 17:52:21 +00:00
);
const afterTwoToggles = reducer(afterOneToggle, toggleSettings());
const afterThreeToggles = reducer(afterTwoToggles, toggleSettings());
assert.isTrue(afterOneToggle.activeCallState?.settingsDialogOpen);
assert.isFalse(afterTwoToggles.activeCallState?.settingsDialogOpen);
assert.isTrue(afterThreeToggles.activeCallState?.settingsDialogOpen);
2020-10-30 17:52:21 +00:00
});
});
2020-10-30 17:52:21 +00:00
describe('toggleParticipants', () => {
const { toggleParticipants } = actions;
it('toggles the participants list', () => {
const afterOneToggle = reducer(
stateWithActiveDirectCall,
toggleParticipants()
);
const afterTwoToggles = reducer(afterOneToggle, toggleParticipants());
const afterThreeToggles = reducer(
afterTwoToggles,
toggleParticipants()
2020-10-30 17:52:21 +00:00
);
2020-11-17 15:07:53 +00:00
assert.isTrue(afterOneToggle.activeCallState?.showParticipantsList);
assert.isFalse(afterTwoToggles.activeCallState?.showParticipantsList);
assert.isTrue(afterThreeToggles.activeCallState?.showParticipantsList);
});
});
describe('togglePip', () => {
const { togglePip } = actions;
it('toggles the PiP', () => {
const afterOneToggle = reducer(stateWithActiveDirectCall, togglePip());
const afterTwoToggles = reducer(afterOneToggle, togglePip());
const afterThreeToggles = reducer(afterTwoToggles, togglePip());
assert.isTrue(afterOneToggle.activeCallState?.pip);
assert.isFalse(afterTwoToggles.activeCallState?.pip);
assert.isTrue(afterThreeToggles.activeCallState?.pip);
2020-10-30 17:52:21 +00:00
});
});
2021-01-08 22:57:54 +00:00
describe('toggleSpeakerView', () => {
const { toggleSpeakerView } = actions;
it('toggles speaker view from grid view', () => {
2021-01-08 22:57:54 +00:00
const afterOneToggle = reducer(
stateWithActiveGroupCall,
toggleSpeakerView()
);
const afterTwoToggles = reducer(afterOneToggle, toggleSpeakerView());
const afterThreeToggles = reducer(afterTwoToggles, toggleSpeakerView());
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Speaker
);
assert.strictEqual(
afterTwoToggles.activeCallState?.viewMode,
CallViewMode.Grid
);
assert.strictEqual(
afterThreeToggles.activeCallState?.viewMode,
CallViewMode.Speaker
);
});
it('toggles speaker view from presentation view', () => {
const afterOneToggle = reducer(
stateWithActivePresentationViewGroupCall,
toggleSpeakerView()
);
const afterTwoToggles = reducer(afterOneToggle, toggleSpeakerView());
const afterThreeToggles = reducer(afterTwoToggles, toggleSpeakerView());
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Grid
);
assert.strictEqual(
afterTwoToggles.activeCallState?.viewMode,
CallViewMode.Speaker
);
assert.strictEqual(
afterThreeToggles.activeCallState?.viewMode,
CallViewMode.Grid
);
});
});
describe('switchToPresentationView', () => {
const { switchToPresentationView, switchFromPresentationView } = actions;
it('toggles presentation view from grid view', () => {
const afterOneToggle = reducer(
stateWithActiveGroupCall,
switchToPresentationView()
);
const afterTwoToggles = reducer(
afterOneToggle,
switchToPresentationView()
);
const finalState = reducer(
afterOneToggle,
switchFromPresentationView()
);
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Presentation
);
assert.strictEqual(
afterTwoToggles.activeCallState?.viewMode,
CallViewMode.Presentation
);
assert.strictEqual(
finalState.activeCallState?.viewMode,
CallViewMode.Grid
);
});
it('does not toggle presentation view from speaker view', () => {
const afterOneToggle = reducer(
stateWithActiveSpeakerViewGroupCall,
switchToPresentationView()
);
const finalState = reducer(
afterOneToggle,
switchFromPresentationView()
);
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Speaker
);
assert.strictEqual(
finalState.activeCallState?.viewMode,
CallViewMode.Speaker
);
2021-01-08 22:57:54 +00:00
});
});
2020-10-30 17:52:21 +00:00
});
2020-11-13 19:57:55 +00:00
describe('helpers', () => {
describe('getActiveCall', () => {
it('returns undefined if there are no calls', () => {
assert.isUndefined(getActiveCall(getEmptyState()));
});
it('returns undefined if there is no active call', () => {
assert.isUndefined(getActiveCall(stateWithDirectCall));
});
it('returns the active call', () => {
assert.deepEqual(getActiveCall(stateWithActiveDirectCall), {
callMode: CallMode.Direct,
conversationId: 'fake-direct-call-conversation-id',
callState: CallState.Accepted,
isIncoming: false,
isVideoCall: false,
hasRemoteVideo: false,
});
});
});
2020-11-20 17:19:28 +00:00
describe('isAnybodyElseInGroupCall', () => {
it('returns false with no peek info', () => {
assert.isFalse(isAnybodyElseInGroupCall(undefined, remoteUuid));
2020-11-20 17:19:28 +00:00
});
it('returns false if the peek info has no participants', () => {
assert.isFalse(isAnybodyElseInGroupCall({ uuids: [] }, remoteUuid));
2020-11-20 17:19:28 +00:00
});
it('returns false if the peek info has one participant, you', () => {
assert.isFalse(
isAnybodyElseInGroupCall({ uuids: [creatorUuid] }, creatorUuid)
2020-11-20 17:19:28 +00:00
);
});
it('returns true if the peek info has one participant, someone else', () => {
assert.isTrue(
isAnybodyElseInGroupCall({ uuids: [creatorUuid] }, remoteUuid)
2020-11-20 17:19:28 +00:00
);
});
it('returns true if the peek info has two participants, you and someone else', () => {
assert.isTrue(
isAnybodyElseInGroupCall(
{ uuids: [creatorUuid, remoteUuid] },
2021-10-26 22:59:08 +00:00
remoteUuid
2020-11-20 17:19:28 +00:00
)
);
});
});
2020-11-13 19:57:55 +00:00
});
2020-10-30 17:52:21 +00:00
});