[MM-59773] Add unit tests for Calls (#8480)
* Unit tests for app/products/calls/observers * Unit tests for app/products/calls/actions * Unit tests for app/products/calls/client * Unit tests for app/products/calls/connection * Unit tests for app/products/calls/connection/websocket_event_handlers.ts * Unit tests for app/products/calls/connection/connection.ts * Unit tests for app/products/calls/state/actions.ts * Unit tests for app/products/calls/utils.ts * Unit tests for app/products/calls/alerts.ts * Unit tests for app/products/calls/calls_manager.ts * Unit tests for app/products/calls/hooks.ts * Factor out force logout error * Reduce use of 'as jest.Mock' * Add test case * Use constants * Better naming * Fix types after merge * Fix test
This commit is contained in:
parent
681585d5db
commit
44bce6c319
17 changed files with 4095 additions and 45 deletions
|
|
@ -5,6 +5,7 @@ import assert from 'assert';
|
|||
|
||||
import {act, renderHook} from '@testing-library/react-hooks'; // Use instead of react-native version due to different behavior. Consider migrating
|
||||
import {createIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import * as CallsActions from '@calls/actions';
|
||||
|
|
@ -25,12 +26,14 @@ import {
|
|||
useChannelsWithCalls,
|
||||
useCurrentCall,
|
||||
userJoinedCall,
|
||||
getCurrentCall,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
type Call,
|
||||
type CallsState,
|
||||
type ChannelsWithCalls,
|
||||
type CurrentCall,
|
||||
AudioDevice,
|
||||
DefaultCallsConfig,
|
||||
DefaultCallsState,
|
||||
} from '@calls/types/calls';
|
||||
|
|
@ -38,6 +41,8 @@ import {errorAlert} from '@calls/utils';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import type {CallJobState} from '@mattermost/calls/lib/types';
|
||||
|
||||
const mockClient = {
|
||||
getCalls: jest.fn(() => [
|
||||
{
|
||||
|
|
@ -54,12 +59,15 @@ const mockClient = {
|
|||
enabled: true,
|
||||
},
|
||||
]),
|
||||
getCallsConfig: jest.fn(() => ({
|
||||
ICEServers: ['mattermost.com'],
|
||||
AllowEnableCalls: true,
|
||||
DefaultEnabled: true,
|
||||
last_retrieved_at: 1234,
|
||||
})),
|
||||
hostMake: jest.fn(),
|
||||
hostMute: jest.fn(),
|
||||
hostMuteOthers: jest.fn(),
|
||||
hostScreenOff: jest.fn(),
|
||||
hostLowerHand: jest.fn(),
|
||||
hostRemove: jest.fn(),
|
||||
endCall: jest.fn(),
|
||||
getCallsConfig: jest.fn(),
|
||||
getCallForChannel: jest.fn(),
|
||||
getVersion: jest.fn(() => ({})),
|
||||
getPluginsManifests: jest.fn(() => (
|
||||
[
|
||||
|
|
@ -79,6 +87,8 @@ jest.mock('@calls/connection/connection', () => ({
|
|||
mute: jest.fn(),
|
||||
unmute: jest.fn(),
|
||||
waitForPeerConnection: jest.fn(() => Promise.resolve()),
|
||||
initializeVoiceTrack: jest.fn(),
|
||||
sendReaction: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
|
|
@ -98,6 +108,7 @@ jest.mock('@calls/alerts', () => {
|
|||
needsRecordingErrorAlert: jest.fn(),
|
||||
needsRecordingWillBePostedAlert: jest.fn(),
|
||||
showErrorAlertOnClose: alerts.showErrorAlertOnClose,
|
||||
leaveAndJoinWithAlert: alerts.leaveAndJoinWithAlert,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -111,6 +122,28 @@ jest.mock('react-native-navigation', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('@actions/remote/session', () => ({
|
||||
forceLogoutIfNecessary: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/user', () => ({
|
||||
getCurrentUser: jest.fn(),
|
||||
getUserById: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/channel', () => ({
|
||||
getChannelById: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/system', () => ({
|
||||
getLicense: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/preference', () => ({
|
||||
queryDisplayNamePreferences: jest.fn(),
|
||||
}));
|
||||
|
||||
const addFakeCall = (serverUrl: string, channelId: string) => {
|
||||
const call: Call = {
|
||||
id: 'call',
|
||||
|
|
@ -172,6 +205,8 @@ describe('Actions.Calls', () => {
|
|||
|
||||
InCallManager.setSpeakerphoneOn = jest.fn();
|
||||
InCallManager.setForceSpeakerphoneOn = jest.fn();
|
||||
InCallManager.chooseAudioRoute = jest.fn();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
NetworkManager.getClient = () => mockClient;
|
||||
|
|
@ -195,8 +230,13 @@ describe('Actions.Calls', () => {
|
|||
newConnection.mockClear();
|
||||
updateThreadFollowing.mockClear();
|
||||
mockClient.getCalls.mockClear();
|
||||
mockClient.getCallsConfig.mockClear();
|
||||
mockClient.getPluginsManifests.mockClear();
|
||||
mockClient.getPluginsManifests = jest.fn(() => (
|
||||
[
|
||||
{id: 'playbooks'},
|
||||
{id: 'com.mattermost.calls'},
|
||||
]
|
||||
));
|
||||
mockClient.enableChannelCalls.mockClear();
|
||||
|
||||
// reset to default state for each test
|
||||
|
|
@ -208,6 +248,9 @@ describe('Actions.Calls', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const forceLogout = require('@actions/remote/session').forceLogoutIfNecessary;
|
||||
const forceLogoutError = {status_code: 401};
|
||||
|
||||
it('joinCall', async () => {
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
|
|
@ -235,6 +278,32 @@ describe('Actions.Calls', () => {
|
|||
await act(async () => {
|
||||
CallsActions.leaveCall();
|
||||
});
|
||||
|
||||
// Test error case
|
||||
newConnection.mockRejectedValueOnce(forceLogoutError);
|
||||
await act(async () => {
|
||||
await expect(CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}))).resolves.toStrictEqual({error: forceLogoutError});
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
|
||||
// Test failure to connect case
|
||||
const connection = {
|
||||
disconnect: jest.fn(),
|
||||
waitForPeerConnection: jest.fn().mockRejectedValueOnce(new Error('failed to connect')),
|
||||
};
|
||||
newConnection.mockResolvedValueOnce(connection);
|
||||
|
||||
await act(async () => {
|
||||
const res = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
expect(res).toStrictEqual({error: 'unable to connect to the voice call: Error: failed to connect'});
|
||||
expect(connection.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('leaveCall', async () => {
|
||||
|
|
@ -346,14 +415,25 @@ describe('Actions.Calls', () => {
|
|||
return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()];
|
||||
});
|
||||
|
||||
// Test successful case
|
||||
await act(async () => {
|
||||
await CallsActions.loadCalls('server1', 'userId1');
|
||||
const successResult = await CallsActions.loadCalls('server1', 'userId1');
|
||||
expect(successResult.data).toBeDefined();
|
||||
expect(mockClient.getCalls).toHaveBeenCalled();
|
||||
assert.equal((result.current[0] as CallsState).calls['channel-1'].channelId, 'channel-1');
|
||||
assert.equal((result.current[0] as CallsState).enabled['channel-1'], true);
|
||||
assert.equal((result.current[1] as ChannelsWithCalls)['channel-1'], true);
|
||||
assert.equal((result.current[2] as CurrentCall | null), null);
|
||||
});
|
||||
|
||||
// Test error case
|
||||
mockClient.getCalls = jest.fn().mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
await act(async () => {
|
||||
const errorResult = await CallsActions.loadCalls('server1', 'userId1');
|
||||
expect(errorResult.error).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
expect(mockClient.getCalls).toHaveBeenCalled();
|
||||
assert.equal((result.current[0] as CallsState).calls['channel-1'].channelId, 'channel-1');
|
||||
assert.equal((result.current[0] as CallsState).enabled['channel-1'], true);
|
||||
assert.equal((result.current[1] as ChannelsWithCalls)['channel-1'], true);
|
||||
assert.equal((result.current[2] as CurrentCall | null), null);
|
||||
});
|
||||
|
||||
it('loadCalls fails from server', async () => {
|
||||
|
|
@ -389,23 +469,53 @@ describe('Actions.Calls', () => {
|
|||
// setup
|
||||
const {result} = renderHook(() => useCallsConfig('server1'));
|
||||
|
||||
// Test successful case
|
||||
await act(async () => {
|
||||
await CallsActions.loadConfig('server1', false, 'Server Switch');
|
||||
mockClient.getCallsConfig.mockReturnValueOnce({DefaultEnabled: true, AllowEnableCalls: true});
|
||||
const successResult = await CallsActions.loadConfig('server1', false, 'Server Switch');
|
||||
expect(successResult.data).toBeDefined();
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledWith('Server Switch');
|
||||
assert.equal(result.current.DefaultEnabled, true);
|
||||
assert.equal(result.current.AllowEnableCalls, true);
|
||||
});
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledWith('Server Switch');
|
||||
assert.equal(result.current.DefaultEnabled, true);
|
||||
assert.equal(result.current.AllowEnableCalls, true);
|
||||
|
||||
// Test successful retrival from cache
|
||||
await act(async () => {
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledTimes(1);
|
||||
const successResult = await CallsActions.loadConfig('server1', false, 'Server Switch');
|
||||
expect(successResult.data).toBeDefined();
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Test error case
|
||||
mockClient.getCallsConfig.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.loadConfig('server1', true, 'Server Switch');
|
||||
expect(errorResult.error).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
|
||||
it('enableChannelCalls', async () => {
|
||||
const {result} = renderHook(() => useCallsState('server1'));
|
||||
|
||||
// Test successful case
|
||||
assert.equal(result.current.enabled['channel-1'], undefined);
|
||||
mockClient.enableChannelCalls.mockReturnValueOnce({enabled: true});
|
||||
await act(async () => {
|
||||
await CallsActions.enableChannelCalls('server1', 'channel-1', true);
|
||||
const successResult = await CallsActions.enableChannelCalls('server1', 'channel-1', true);
|
||||
expect(successResult).toEqual({});
|
||||
expect(mockClient.enableChannelCalls).toHaveBeenCalledWith('channel-1', true);
|
||||
assert.equal(result.current.enabled['channel-1'], true);
|
||||
});
|
||||
|
||||
// Test error case
|
||||
mockClient.enableChannelCalls.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
await act(async () => {
|
||||
const errorResult = await CallsActions.enableChannelCalls('server1', 'channel-1', true);
|
||||
expect(errorResult.error).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
expect(mockClient.enableChannelCalls).toHaveBeenCalledWith('channel-1', true);
|
||||
assert.equal(result.current.enabled['channel-1'], true);
|
||||
});
|
||||
|
||||
it('disableChannelCalls', async () => {
|
||||
|
|
@ -427,29 +537,710 @@ describe('Actions.Calls', () => {
|
|||
|
||||
it('startCallRecording', async () => {
|
||||
await act(async () => {
|
||||
await CallsActions.startCallRecording('server1', 'channel-id');
|
||||
});
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.startCallRecording('server1', 'channel-id');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.startCallRecording).toHaveBeenCalledWith('channel-id');
|
||||
expect(needsRecordingErrorAlert).toHaveBeenCalled();
|
||||
|
||||
expect(mockClient.startCallRecording).toHaveBeenCalledWith('channel-id');
|
||||
expect(needsRecordingErrorAlert).toHaveBeenCalled();
|
||||
// Test error case
|
||||
mockClient.startCallRecording.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.startCallRecording('server1', 'channel-id');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
it('stopCallRecording', async () => {
|
||||
await act(async () => {
|
||||
await CallsActions.stopCallRecording('server1', 'channel-id');
|
||||
});
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.stopCallRecording('server1', 'channel-id');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.stopCallRecording).toHaveBeenCalledWith('channel-id');
|
||||
expect(needsRecordingErrorAlert).toHaveBeenCalled();
|
||||
expect(needsRecordingWillBePostedAlert).toHaveBeenCalled();
|
||||
|
||||
expect(mockClient.stopCallRecording).toHaveBeenCalledWith('channel-id');
|
||||
expect(needsRecordingErrorAlert).toHaveBeenCalled();
|
||||
expect(needsRecordingWillBePostedAlert).toHaveBeenCalled();
|
||||
// Test error case
|
||||
mockClient.stopCallRecording.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.stopCallRecording('server1', 'channel-id');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
it('dismissIncomingCall', async () => {
|
||||
// Test successful case
|
||||
await act(async () => {
|
||||
await CallsActions.dismissIncomingCall('server1', 'channel-id');
|
||||
});
|
||||
|
||||
expect(mockClient.dismissCall).toHaveBeenCalledWith('channel-id');
|
||||
|
||||
// Test error case
|
||||
mockClient.dismissCall.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.dismissIncomingCall('server1', 'channel-id');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
|
||||
// Test when ringing is disabled
|
||||
act(() => {
|
||||
setCallsConfig('server1', {...DefaultCallsConfig, EnableRinging: false});
|
||||
});
|
||||
|
||||
const result = await CallsActions.dismissIncomingCall('server1', 'channel-id');
|
||||
expect(result).toEqual({});
|
||||
expect(mockClient.dismissCall).toHaveBeenCalledTimes(2); // Called twice - success case and error case
|
||||
});
|
||||
|
||||
it('hostMake', async () => {
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.hostMake('server1', 'call1', 'user1');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.hostMake).toHaveBeenCalledWith('call1', 'user1');
|
||||
|
||||
// Test error case
|
||||
mockClient.hostMake.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.hostMake('server1', 'call1', 'user1');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
|
||||
it('hostMuteSession', async () => {
|
||||
// Test successful case
|
||||
const successfulResult = await CallsActions.hostMuteSession('server1', 'call1', 'session1');
|
||||
expect(successfulResult).toBe(undefined);
|
||||
expect(mockClient.hostMute).toHaveBeenCalledWith('call1', 'session1');
|
||||
|
||||
// Test error case
|
||||
mockClient.hostMute.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.hostMuteSession('server1', 'call1', 'session1');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
|
||||
it('hostMuteOthers', async () => {
|
||||
await act(async () => {
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.hostMuteOthers('server1', 'call1');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.hostMuteOthers).toHaveBeenCalledWith('call1');
|
||||
|
||||
// Test error case
|
||||
mockClient.hostMuteOthers.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.hostMuteOthers('server1', 'call1');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
it('hostStopScreenshare', async () => {
|
||||
await act(async () => {
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.hostStopScreenshare('server1', 'call1', 'session1');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.hostScreenOff).toHaveBeenCalledWith('call1', 'session1');
|
||||
|
||||
// Test error case
|
||||
mockClient.hostScreenOff.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.hostStopScreenshare('server1', 'call1', 'session1');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
it('hostLowerHand', async () => {
|
||||
await act(async () => {
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.hostLowerHand('server1', 'call1', 'session1');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.hostLowerHand).toHaveBeenCalledWith('call1', 'session1');
|
||||
|
||||
// Test error case
|
||||
mockClient.hostLowerHand.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.hostLowerHand('server1', 'call1', 'session1');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
it('hostRemove', async () => {
|
||||
await act(async () => {
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.hostRemove('server1', 'call1', 'session1');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.hostRemove).toHaveBeenCalledWith('call1', 'session1');
|
||||
|
||||
// Test error case
|
||||
mockClient.hostRemove.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.hostRemove('server1', 'call1', 'session1');
|
||||
expect(errorResult).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallsSlashCommand', () => {
|
||||
const intl = createIntl({locale: 'en', messages: {}});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
act(() => {
|
||||
setCallsState('server1', DefaultCallsState);
|
||||
setChannelsWithCalls('server1', {});
|
||||
setCurrentCall(null);
|
||||
setCallsConfig('server1', DefaultCallsConfig);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-call commands', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/other command', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result).toEqual({handled: false});
|
||||
});
|
||||
|
||||
it('should handle /call without subcommand', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result).toEqual({handled: false});
|
||||
});
|
||||
|
||||
it('should handle end command', async () => {
|
||||
const getCurrentUser = require('@queries/servers/user').getCurrentUser;
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user1',
|
||||
roles: 'system_admin',
|
||||
});
|
||||
|
||||
jest.spyOn(Alert, 'alert');
|
||||
|
||||
await CallsActions.handleCallsSlashCommand('/call end', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle start command in DM', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call start Test Call', 'server1', 'channel1', 'D', 'root1', 'user1', intl);
|
||||
expect(result).toEqual({handled: true});
|
||||
});
|
||||
|
||||
it('should handle start command with group calls disabled', async () => {
|
||||
act(() => {
|
||||
setCallsConfig('server1', {...DefaultCallsConfig, DefaultEnabled: false, AllowEnableCalls: false});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call start', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle start command with existing call', async () => {
|
||||
act(() => {
|
||||
setChannelsWithCalls('server1', {channel1: true});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call start', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle join command', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call join Test Call', 'server1', 'channel1', 'D', 'root1', 'user1', intl);
|
||||
expect(result).toEqual({handled: true});
|
||||
});
|
||||
|
||||
it('should handle leave command when in call', async () => {
|
||||
act(() => {
|
||||
newCurrentCall('server1', 'channel1', 'user1');
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call leave', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result).toEqual({handled: true});
|
||||
});
|
||||
|
||||
it('should handle leave command when not in call', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call leave', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('recording commands', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
newCurrentCall('server1', 'channel1', 'user1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleEndCall', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Alert, 'alert');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should show error when user lacks permission', async () => {
|
||||
const getCurrentUser = require('@queries/servers/user').getCurrentUser;
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user2',
|
||||
roles: 'user',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
State.setCallsState('server1', {
|
||||
myUserId: 'user2',
|
||||
calls: {
|
||||
channel1: {
|
||||
hostId: 'user1',
|
||||
} as Call,
|
||||
},
|
||||
enabled: {},
|
||||
});
|
||||
});
|
||||
|
||||
await CallsActions.handleCallsSlashCommand('/call end', 'server1', 'channel1', 'O', '', 'user2', intl);
|
||||
|
||||
expect(Alert.alert).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'You don\'t have permission to end the call. Please ask the call owner to end the call.',
|
||||
);
|
||||
expect(mockClient.endCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should end call when user has permission', async () => {
|
||||
const getCurrentUser = require('@queries/servers/user').getCurrentUser;
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user1',
|
||||
roles: 'system_admin',
|
||||
});
|
||||
|
||||
const getChannelById = require('@queries/servers/channel').getChannelById;
|
||||
getChannelById.mockResolvedValueOnce({
|
||||
id: 'channel1',
|
||||
type: 'O',
|
||||
displayName: 'Test Channel',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
State.setCallsState('server1', {
|
||||
myUserId: 'user1',
|
||||
calls: {
|
||||
channel1: {
|
||||
id: 'call1',
|
||||
sessions: {
|
||||
session1: {
|
||||
sessionId: 'session1',
|
||||
userId: 'user1',
|
||||
muted: false,
|
||||
raisedHand: 0,
|
||||
},
|
||||
session2: {
|
||||
sessionId: 'session2',
|
||||
userId: 'user2',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
},
|
||||
},
|
||||
channelId: 'channel1',
|
||||
startTime: (new Date()).getTime(),
|
||||
screenOn: '',
|
||||
threadId: 'abcd1234567',
|
||||
ownerId: 'xohi8cki9787fgiryne716u84o',
|
||||
hostId: 'xohi8cki9787fgiryne716u84o',
|
||||
dismissed: {},
|
||||
},
|
||||
},
|
||||
enabled: {},
|
||||
});
|
||||
});
|
||||
|
||||
await CallsActions.handleCallsSlashCommand('/call end', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
|
||||
// Get the callback from the second button
|
||||
const alertCallback = (Alert.alert as jest.Mock).mock.calls[0][2][1].onPress;
|
||||
await alertCallback();
|
||||
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(mockClient.endCall).toHaveBeenCalledWith('channel1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle recording without action', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result).toEqual({handled: false});
|
||||
});
|
||||
|
||||
it('should handle recording start when already recording', async () => {
|
||||
act(() => {
|
||||
setCurrentCall({
|
||||
...getCurrentCall()!,
|
||||
recState: {
|
||||
init_at: 1,
|
||||
start_at: 2,
|
||||
end_at: 1,
|
||||
} as CallJobState,
|
||||
});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording start', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle recording start without host permission', async () => {
|
||||
act(() => {
|
||||
setCurrentCall({
|
||||
...getCurrentCall()!,
|
||||
hostId: 'other_user',
|
||||
});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording start', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle recording start as host', async () => {
|
||||
act(() => {
|
||||
setCurrentCall({
|
||||
...getCurrentCall()!,
|
||||
hostId: 'user1',
|
||||
});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording start', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result).toEqual({handled: true});
|
||||
expect(mockClient.startCallRecording).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle recording stop when not recording', async () => {
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording stop', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle recording stop without host permission', async () => {
|
||||
act(() => {
|
||||
setCurrentCall({
|
||||
...getCurrentCall()!,
|
||||
hostId: 'other_user',
|
||||
recState: {
|
||||
init_at: 1,
|
||||
start_at: 2,
|
||||
end_at: 1,
|
||||
} as CallJobState,
|
||||
});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording stop', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.handled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle recording stop as host', async () => {
|
||||
act(() => {
|
||||
setCurrentCall({
|
||||
...getCurrentCall()!,
|
||||
hostId: 'user1',
|
||||
recState: {
|
||||
init_at: 1,
|
||||
start_at: 2,
|
||||
end_at: 1,
|
||||
} as CallJobState,
|
||||
});
|
||||
});
|
||||
|
||||
const result = await CallsActions.handleCallsSlashCommand('/call recording stop', 'server1', 'channel1', 'O', '', 'user1', intl);
|
||||
expect(result).toEqual({handled: true});
|
||||
expect(mockClient.stopCallRecording).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('loadCallForChannel', async () => {
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useChannelsWithCalls('server1')];
|
||||
});
|
||||
|
||||
// Test successful case
|
||||
await act(async () => {
|
||||
mockClient.getCallForChannel.mockReturnValueOnce({
|
||||
call: {
|
||||
channel_id: 'channel-1',
|
||||
users: ['user-1'],
|
||||
},
|
||||
enabled: true});
|
||||
const successResult = await CallsActions.loadCallForChannel('server1', 'channel-1');
|
||||
expect(successResult.data).toBeDefined();
|
||||
expect(mockClient.getCallForChannel).toHaveBeenCalled();
|
||||
assert.equal((result.current[0].calls as Dictionary<Call>)['channel-1'].channelId, 'channel-1');
|
||||
assert.equal((result.current[0].enabled as Dictionary<boolean>)['channel-1'], true);
|
||||
});
|
||||
|
||||
// Test error case
|
||||
mockClient.getCallForChannel.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
await act(async () => {
|
||||
const errorResult = await CallsActions.loadCallForChannel('server1', 'channel-1');
|
||||
expect(errorResult.error).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
});
|
||||
|
||||
it('loadConfigAndCalls', async () => {
|
||||
// Test successful case - plugin enabled
|
||||
const successResult = await CallsActions.loadConfigAndCalls('server1', 'user1');
|
||||
expect(successResult).toBeUndefined();
|
||||
expect(mockClient.getPluginsManifests).toHaveBeenCalled();
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.getCalls).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test when plugin is disabled
|
||||
mockClient.getPluginsManifests.mockReturnValueOnce([{id: 'other-plugin'}]);
|
||||
const disabledResult = await CallsActions.loadConfigAndCalls('server1', 'user1');
|
||||
expect(disabledResult).toBeUndefined();
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.getCalls).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('checkIsCallsPluginEnabled', async () => {
|
||||
// Test successful case - plugin enabled
|
||||
const successResult = await CallsActions.checkIsCallsPluginEnabled('server1');
|
||||
expect(successResult.data).toBe(true);
|
||||
expect(mockClient.getPluginsManifests).toHaveBeenCalled();
|
||||
|
||||
// Test successful case - plugin disabled
|
||||
mockClient.getPluginsManifests.mockReturnValueOnce([{id: 'other-plugin'}]);
|
||||
const disabledResult = await CallsActions.checkIsCallsPluginEnabled('server1');
|
||||
expect(disabledResult.data).toBe(false);
|
||||
|
||||
// Test error case
|
||||
mockClient.getPluginsManifests = jest.fn().mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
const errorResult = await CallsActions.checkIsCallsPluginEnabled('server1');
|
||||
expect(errorResult.error).toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
|
||||
it('canEndCall', async () => {
|
||||
// Test when server cannot be found.
|
||||
const result1 = await CallsActions.canEndCall('server2', 'channel-1');
|
||||
expect(result1).toBe(false);
|
||||
|
||||
await act(() => {
|
||||
State.setCallsState('server1', {
|
||||
myUserId: 'user1',
|
||||
calls: {
|
||||
'channel-1': {
|
||||
hostId: 'user1',
|
||||
} as Call,
|
||||
},
|
||||
enabled: {},
|
||||
});
|
||||
});
|
||||
|
||||
// Test when current user is system admin
|
||||
const getCurrentUser = require('@queries/servers/user').getCurrentUser;
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user1',
|
||||
roles: 'system_admin',
|
||||
});
|
||||
|
||||
const result2 = await CallsActions.canEndCall('server1', 'channel-1');
|
||||
expect(result2).toBe(true);
|
||||
|
||||
// Test when current user is call host
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user1',
|
||||
roles: 'user',
|
||||
});
|
||||
|
||||
const result3 = await CallsActions.canEndCall('server1', 'channel-1');
|
||||
expect(result3).toBe(true);
|
||||
|
||||
// Test when current user is neither admin nor host
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user2',
|
||||
roles: 'user',
|
||||
});
|
||||
|
||||
const result4 = await CallsActions.canEndCall('server1', 'channel-1');
|
||||
expect(result4).toBe(false);
|
||||
});
|
||||
|
||||
it('getEndCallMessage', async () => {
|
||||
const intl = createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
});
|
||||
|
||||
const getChannelById = require('@queries/servers/channel').getChannelById;
|
||||
const getUserById = require('@queries/servers/user').getUserById;
|
||||
const getLicense = require('@queries/servers/system').getLicense;
|
||||
const getConfig = require('@queries/servers/system').getConfig;
|
||||
const queryDisplayNamePreferences = require('@queries/servers/preference').queryDisplayNamePreferences;
|
||||
|
||||
// Test when server cannot be found.
|
||||
const result1 = await CallsActions.getEndCallMessage('server2', 'channel-1', 'user1', intl);
|
||||
expect(result1).toContain('Are you sure you want to end the call?');
|
||||
|
||||
// Test regular channel
|
||||
getChannelById.mockResolvedValueOnce({
|
||||
id: 'channel-1',
|
||||
type: 'O',
|
||||
displayName: 'Test Channel',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
State.setCallsState('server1', {
|
||||
myUserId: 'user1',
|
||||
calls: {
|
||||
'channel-1': {
|
||||
id: 'call1',
|
||||
sessions: {
|
||||
session1: {
|
||||
sessionId: 'a23456abcdefghijklmnopqrs',
|
||||
userId: 'xohi8cki9787fgiryne716u84o',
|
||||
muted: false,
|
||||
raisedHand: 0,
|
||||
},
|
||||
session2: {
|
||||
sessionId: 'a12345667890bcdefghijklmn1',
|
||||
userId: 'xohi8cki9787fgiryne716u84o',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
},
|
||||
},
|
||||
channelId: 'channel-1',
|
||||
startTime: (new Date()).getTime(),
|
||||
screenOn: '',
|
||||
threadId: 'abcd1234567',
|
||||
ownerId: 'xohi8cki9787fgiryne716u84o',
|
||||
hostId: 'xohi8cki9787fgiryne716u84o',
|
||||
dismissed: {},
|
||||
},
|
||||
},
|
||||
enabled: {},
|
||||
});
|
||||
});
|
||||
|
||||
const result2 = await CallsActions.getEndCallMessage('server1', 'channel-1', 'user1', intl);
|
||||
expect(result2).toContain('2 participants');
|
||||
expect(result2).toContain('Test Channel');
|
||||
|
||||
act(() => {
|
||||
State.setCallsState('server1', {
|
||||
myUserId: 'user1',
|
||||
calls: {
|
||||
'channel-2': {
|
||||
id: 'call1',
|
||||
sessions: {
|
||||
session1: {
|
||||
sessionId: 'a23456abcdefghijklmnopqrs',
|
||||
userId: 'xohi8cki9787fgiryne716u84o',
|
||||
muted: false,
|
||||
raisedHand: 0,
|
||||
},
|
||||
session2: {
|
||||
sessionId: 'a12345667890bcdefghijklmn1',
|
||||
userId: 'xohi8cki9787fgiryne716u84o',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
},
|
||||
},
|
||||
channelId: 'channel-2',
|
||||
startTime: (new Date()).getTime(),
|
||||
screenOn: '',
|
||||
threadId: 'abcd1234567',
|
||||
ownerId: 'xohi8cki9787fgiryne716u84o',
|
||||
hostId: 'xohi8cki9787fgiryne716u84o',
|
||||
dismissed: {},
|
||||
},
|
||||
},
|
||||
enabled: {},
|
||||
});
|
||||
});
|
||||
|
||||
// Test DM channel
|
||||
getChannelById.mockResolvedValueOnce({
|
||||
id: 'channel-2',
|
||||
type: 'D',
|
||||
name: 'user1__user2',
|
||||
displayName: 'User Two',
|
||||
});
|
||||
|
||||
getUserById.mockResolvedValueOnce({
|
||||
id: 'user2',
|
||||
username: 'user2',
|
||||
firstName: 'User',
|
||||
lastName: 'Two',
|
||||
});
|
||||
|
||||
getLicense.mockResolvedValueOnce({});
|
||||
getConfig.mockResolvedValueOnce({
|
||||
TeammateNameDisplay: 'username',
|
||||
});
|
||||
queryDisplayNamePreferences.mockReturnValueOnce({
|
||||
fetch: () => Promise.resolve([]),
|
||||
});
|
||||
|
||||
const result3 = await CallsActions.getEndCallMessage('server1', 'channel-2', 'user1', intl);
|
||||
expect(result3).toBe('Are you sure you want to end the call with user2?');
|
||||
});
|
||||
|
||||
it('endCall', async () => {
|
||||
// Test successful case
|
||||
const successResult = await CallsActions.endCall('server1', 'channel-1');
|
||||
expect(successResult).toBe(undefined);
|
||||
expect(mockClient.endCall).toHaveBeenCalledWith('channel-1');
|
||||
|
||||
// Test error case
|
||||
mockClient.endCall.mockRejectedValueOnce(forceLogoutError);
|
||||
|
||||
await expect(CallsActions.endCall('server1', 'channel-1')).rejects.toBe(forceLogoutError);
|
||||
expect(forceLogout).toHaveBeenCalledWith('server1', forceLogoutError);
|
||||
});
|
||||
|
||||
it('setPreferredAudioRoute', async () => {
|
||||
await CallsActions.setPreferredAudioRoute(AudioDevice.Speakerphone);
|
||||
expect(InCallManager.chooseAudioRoute).toHaveBeenCalledWith('SPEAKER_PHONE');
|
||||
});
|
||||
|
||||
it('initializeVoiceTrack', async () => {
|
||||
renderHook(() => useCurrentCall());
|
||||
addFakeCall('server1', 'channel-id');
|
||||
|
||||
await act(async () => {
|
||||
await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
CallsActions.initializeVoiceTrack();
|
||||
expect(getConnectionForTesting()?.initializeVoiceTrack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sendReaction', async () => {
|
||||
renderHook(() => useCurrentCall());
|
||||
addFakeCall('server1', 'channel-id');
|
||||
|
||||
await act(async () => {
|
||||
await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
const emoji = {name: 'smile', unified: '1f604'};
|
||||
CallsActions.sendReaction(emoji);
|
||||
expect(getConnectionForTesting()?.sendReaction).toHaveBeenCalledWith(emoji);
|
||||
});
|
||||
|
||||
it('userLeftChannelErr', async () => {
|
||||
|
|
|
|||
|
|
@ -669,7 +669,7 @@ const handleEndCall = async (serverUrl: string, channelId: string, currentUserId
|
|||
export const hostMake = async (serverUrl: string, callId: string, newHostId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.hostMake(callId, newHostId);
|
||||
return await client.hostMake(callId, newHostId);
|
||||
} catch (error) {
|
||||
logDebug('error on hostMake', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
@ -680,7 +680,7 @@ export const hostMake = async (serverUrl: string, callId: string, newHostId: str
|
|||
export const hostMuteSession = async (serverUrl: string, callId: string, sessionId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.hostMute(callId, sessionId);
|
||||
return await client.hostMute(callId, sessionId);
|
||||
} catch (error) {
|
||||
logDebug('error on hostMute', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
@ -691,7 +691,7 @@ export const hostMuteSession = async (serverUrl: string, callId: string, session
|
|||
export const hostMuteOthers = async (serverUrl: string, callId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.hostMuteOthers(callId);
|
||||
return await client.hostMuteOthers(callId);
|
||||
} catch (error) {
|
||||
logDebug('error on hostMuteOthers', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
@ -702,7 +702,7 @@ export const hostMuteOthers = async (serverUrl: string, callId: string) => {
|
|||
export const hostStopScreenshare = async (serverUrl: string, callId: string, sessionId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.hostScreenOff(callId, sessionId);
|
||||
return await client.hostScreenOff(callId, sessionId);
|
||||
} catch (error) {
|
||||
logDebug('error on hostStopScreenshare', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
@ -713,7 +713,7 @@ export const hostStopScreenshare = async (serverUrl: string, callId: string, ses
|
|||
export const hostLowerHand = async (serverUrl: string, callId: string, sessionId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.hostLowerHand(callId, sessionId);
|
||||
return await client.hostLowerHand(callId, sessionId);
|
||||
} catch (error) {
|
||||
logDebug('error on hostLowerHand', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
@ -724,7 +724,7 @@ export const hostLowerHand = async (serverUrl: string, callId: string, sessionId
|
|||
export const hostRemove = async (serverUrl: string, callId: string, sessionId: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.hostRemove(callId, sessionId);
|
||||
return await client.hostRemove(callId, sessionId);
|
||||
} catch (error) {
|
||||
logDebug('error on hostRemove', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@ export {
|
|||
startCallRecording,
|
||||
stopCallRecording,
|
||||
dismissIncomingCall,
|
||||
hostMake,
|
||||
hostMuteSession,
|
||||
hostMuteOthers,
|
||||
hostStopScreenshare,
|
||||
hostLowerHand,
|
||||
hostRemove,
|
||||
setPreferredAudioRoute,
|
||||
initializeVoiceTrack,
|
||||
sendReaction,
|
||||
endCall,
|
||||
loadCallForChannel,
|
||||
loadConfigAndCalls,
|
||||
checkIsCallsPluginEnabled,
|
||||
canEndCall,
|
||||
getEndCallMessage,
|
||||
} from './calls';
|
||||
|
||||
export {hasMicrophonePermission} from './permissions';
|
||||
|
|
|
|||
93
app/products/calls/actions/permissions.test.ts
Normal file
93
app/products/calls/actions/permissions.test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {hasBluetoothPermission, hasMicrophonePermission} from './permissions';
|
||||
|
||||
jest.mock('react-native-permissions', () => ({
|
||||
PERMISSIONS: {
|
||||
IOS: {
|
||||
BLUETOOTH: 'ios.bluetooth',
|
||||
MICROPHONE: 'ios.microphone',
|
||||
},
|
||||
ANDROID: {
|
||||
BLUETOOTH_CONNECT: 'android.bluetooth_connect',
|
||||
RECORD_AUDIO: 'android.record_audio',
|
||||
},
|
||||
},
|
||||
RESULTS: {
|
||||
DENIED: 'denied',
|
||||
GRANTED: 'granted',
|
||||
BLOCKED: 'blocked',
|
||||
UNAVAILABLE: 'unavailable',
|
||||
},
|
||||
check: jest.fn(),
|
||||
request: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Permissions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Platform.select = jest.fn((options) => options.ios);
|
||||
});
|
||||
|
||||
describe('hasBluetoothPermission', () => {
|
||||
it('should return true when permission is granted', async () => {
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
const result = await hasBluetoothPermission();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should request permission when denied', async () => {
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.DENIED);
|
||||
jest.mocked(Permissions.request).mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
const result = await hasBluetoothPermission();
|
||||
expect(result).toBe(true);
|
||||
expect(Permissions.request).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when blocked', async () => {
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.BLOCKED);
|
||||
const result = await hasBluetoothPermission();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle Android permissions', async () => {
|
||||
Platform.select = jest.fn((options: Record<string, unknown>) => options.default);
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
await hasBluetoothPermission();
|
||||
expect(Permissions.check).toHaveBeenCalledWith(Permissions.PERMISSIONS.ANDROID.BLUETOOTH_CONNECT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMicrophonePermission', () => {
|
||||
it('should return true when permission is granted', async () => {
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
const result = await hasMicrophonePermission();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should request permission when denied', async () => {
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.DENIED);
|
||||
jest.mocked(Permissions.request).mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
const result = await hasMicrophonePermission();
|
||||
expect(result).toBe(true);
|
||||
expect(Permissions.request).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when blocked', async () => {
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.BLOCKED);
|
||||
const result = await hasMicrophonePermission();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle Android permissions', async () => {
|
||||
Platform.select = jest.fn((options: Record<string, unknown>) => options.default);
|
||||
jest.mocked(Permissions.check).mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
await hasMicrophonePermission();
|
||||
expect(Permissions.check).toHaveBeenCalledWith(Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO);
|
||||
});
|
||||
});
|
||||
});
|
||||
503
app/products/calls/alerts.test.ts
Normal file
503
app/products/calls/alerts.test.ts
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {hostRemove} from '@calls/actions/calls';
|
||||
import {EndCallReturn} from '@calls/types/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
import {
|
||||
showLimitRestrictedAlert,
|
||||
recordingAlert,
|
||||
stopRecordingConfirmationAlert,
|
||||
recordingWillBePostedAlert,
|
||||
recordingErrorAlert,
|
||||
removeFromCall,
|
||||
endCallConfirmationAlert,
|
||||
needsRecordingAlert,
|
||||
needsRecordingWillBePostedAlert,
|
||||
needsRecordingErrorAlert,
|
||||
leaveAndJoinWithAlert,
|
||||
} from './alerts';
|
||||
|
||||
jest.mock('@calls/actions', () => ({
|
||||
leaveCall: jest.fn(),
|
||||
joinCall: jest.fn().mockResolvedValue({}),
|
||||
hasMicrophonePermission: jest.fn().mockResolvedValue(true),
|
||||
unmuteMyself: jest.fn(),
|
||||
dismissIncomingCall: jest.fn(),
|
||||
removeIncomingCall: jest.fn(),
|
||||
}));
|
||||
jest.mock('@calls/actions/permissions', () => ({
|
||||
hasBluetoothPermission: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
jest.mock('@calls/state', () => ({
|
||||
getCallsState: jest.fn(),
|
||||
getChannelsWithCalls: jest.fn(),
|
||||
getCurrentCall: jest.fn(),
|
||||
getCallsConfig: jest.fn(),
|
||||
setMicPermissionsGranted: jest.fn(),
|
||||
}));
|
||||
jest.mock('@calls/actions/calls');
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
dismissBottomSheet: jest.fn(),
|
||||
}));
|
||||
jest.mock('@queries/servers/channel');
|
||||
jest.mock('@queries/servers/user', () => ({
|
||||
getUserById: jest.fn(() => Promise.resolve({
|
||||
username: 'user-1',
|
||||
})),
|
||||
getCurrentUser: jest.fn().mockResolvedValue({
|
||||
username: 'user-1',
|
||||
roles: 'system_user',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('alerts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('showLimitRestrictedAlert', () => {
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}, values?: any) => {
|
||||
if (values) {
|
||||
return defaultMessage.replace('{maxParticipants}', values.maxParticipants);
|
||||
}
|
||||
return defaultMessage;
|
||||
},
|
||||
};
|
||||
|
||||
it('shows alert for cloud starter', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
showLimitRestrictedAlert({
|
||||
limitRestricted: true,
|
||||
maxParticipants: 8,
|
||||
isCloudStarter: true,
|
||||
}, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'This call is at capacity',
|
||||
'Upgrade to Cloud Professional or Cloud Enterprise to enable group calls with more than 8 participants.',
|
||||
[
|
||||
{
|
||||
text: 'Okay',
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('shows alert for non-cloud starter', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
showLimitRestrictedAlert({
|
||||
limitRestricted: true,
|
||||
maxParticipants: 8,
|
||||
isCloudStarter: false,
|
||||
}, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'This call is at capacity',
|
||||
'The maximum number of participants per call is 8. Contact your System Admin to increase the limit.',
|
||||
[
|
||||
{
|
||||
text: 'Okay',
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordingAlert', () => {
|
||||
beforeEach(() => {
|
||||
needsRecordingAlert();
|
||||
});
|
||||
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}) => defaultMessage,
|
||||
};
|
||||
|
||||
it('shows host recording alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingAlert(true, false, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'You are recording',
|
||||
'Consider letting everyone know that this meeting is being recorded.',
|
||||
[{
|
||||
text: 'Dismiss',
|
||||
}],
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show recording alert twice', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingAlert(true, false, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'You are recording',
|
||||
'Consider letting everyone know that this meeting is being recorded.',
|
||||
[{
|
||||
text: 'Dismiss',
|
||||
}],
|
||||
);
|
||||
|
||||
mockAlert.mockClear();
|
||||
recordingAlert(true, false, intl as any);
|
||||
expect(mockAlert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows participant recording alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingAlert(false, false, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Recording is in progress',
|
||||
'The host has started recording this meeting. By staying in the meeting you give consent to being recorded.',
|
||||
[
|
||||
{
|
||||
text: 'Leave',
|
||||
onPress: expect.any(Function),
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: 'Okay',
|
||||
style: 'default',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('shows host transcription alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingAlert(true, true, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Recording and transcription has started',
|
||||
'Consider letting everyone know that this meeting is being recorded and transcribed.',
|
||||
[{
|
||||
text: 'Dismiss',
|
||||
}],
|
||||
);
|
||||
});
|
||||
|
||||
it('shows participant transcription alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingAlert(false, true, intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Recording and transcription is in progress',
|
||||
'The host has started recording and transcription for this meeting. By staying in the meeting, you give consent to being recorded and transcribed.',
|
||||
[
|
||||
{
|
||||
text: 'Leave',
|
||||
onPress: expect.any(Function),
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: 'Okay',
|
||||
style: 'default',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('calls leaveCall when Leave is pressed', () => {
|
||||
const {leaveCall} = require('@calls/actions');
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingAlert(false, false, intl as any);
|
||||
|
||||
const leaveButton = mockAlert.mock.calls[0][2]![0];
|
||||
leaveButton.onPress?.();
|
||||
|
||||
expect(leaveCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopRecordingConfirmationAlert', () => {
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}) => defaultMessage,
|
||||
};
|
||||
|
||||
it('shows recording confirmation alert', async () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
const promise = stopRecordingConfirmationAlert(intl as any, false);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Stop recording',
|
||||
'The call recording will be processed and posted in the call thread. Are you sure you want to stop the recording?',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
onPress: expect.any(Function),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Stop recording',
|
||||
onPress: expect.any(Function),
|
||||
style: 'destructive',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const confirmButton = mockAlert.mock.calls[0][2]![1];
|
||||
confirmButton.onPress?.();
|
||||
expect(await promise).toBe(true);
|
||||
});
|
||||
|
||||
it('shows transcription confirmation alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
stopRecordingConfirmationAlert(intl as any, true);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Stop recording and transcription',
|
||||
'The call recording and transcription files will be processed and posted in the call thread. Are you sure you want to stop the recording and transcription?',
|
||||
expect.any(Array),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordingWillBePostedAlert', () => {
|
||||
beforeEach(() => {
|
||||
needsRecordingWillBePostedAlert();
|
||||
});
|
||||
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}) => defaultMessage,
|
||||
};
|
||||
|
||||
it('shows recording posted alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingWillBePostedAlert(intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Recording has stopped. Processing...',
|
||||
'You can find the recording in this call\'s chat thread once it\'s finished processing.',
|
||||
[{
|
||||
text: 'Dismiss',
|
||||
}],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordingErrorAlert', () => {
|
||||
beforeEach(() => {
|
||||
needsRecordingErrorAlert();
|
||||
});
|
||||
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}) => defaultMessage,
|
||||
};
|
||||
|
||||
it('shows recording error alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
recordingErrorAlert(intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Something went wrong with the recording',
|
||||
'Please try to record again. You can also contact your system admin for troubleshooting help.',
|
||||
[{
|
||||
text: 'Dismiss',
|
||||
}],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromCall', () => {
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}, values?: any) => {
|
||||
if (values) {
|
||||
return defaultMessage.replace('{displayName}', values.displayName);
|
||||
}
|
||||
return defaultMessage;
|
||||
},
|
||||
};
|
||||
|
||||
it('shows remove confirmation and removes user', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
removeFromCall('server1', 'test-user', 'call1', 'session1', intl as any);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Remove participant',
|
||||
'Are you sure you want to remove test-user from the call? ',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Remove',
|
||||
onPress: expect.any(Function),
|
||||
style: 'destructive',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const removeButton = mockAlert.mock.calls[0][2]![1];
|
||||
removeButton.onPress?.();
|
||||
|
||||
expect(hostRemove).toHaveBeenCalledWith('server1', 'call1', 'session1');
|
||||
expect(dismissBottomSheet).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaveAndJoinWithAlert', () => {
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}, values?: any) => {
|
||||
if (values) {
|
||||
return defaultMessage.replace('{leaveChannelName}', values.leaveChannelName).replace('{joinChannelName}', values.joinChannelName);
|
||||
}
|
||||
return defaultMessage;
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await DatabaseManager.init(['server1']);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const {getChannelById} = require('@queries/servers/channel');
|
||||
getChannelById.mockImplementation((db: any, channelId: string) => {
|
||||
if (channelId === 'join-channel') {
|
||||
return {displayName: 'join-channel', type: 'D'};
|
||||
} else if (channelId === 'leave-channel') {
|
||||
return {displayName: 'leave-channel'};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
it('joins new call when not in a call', async () => {
|
||||
const {getCallsState, getChannelsWithCalls, getCurrentCall, getCallsConfig, setMicPermissionsGranted} = require('@calls/state');
|
||||
getCallsState.mockReturnValue({
|
||||
enabled: {
|
||||
'join-channel': true,
|
||||
},
|
||||
});
|
||||
getChannelsWithCalls.mockReturnValue({});
|
||||
getCurrentCall.mockReturnValue(null);
|
||||
getCallsConfig.mockReturnValue({});
|
||||
setMicPermissionsGranted.mockReturnValue(true);
|
||||
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
const result = await leaveAndJoinWithAlert(intl as any, 'server1', 'join-channel');
|
||||
expect(mockAlert).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('shows leave and join confirmation when in a call', async () => {
|
||||
const {getCallsState, getChannelsWithCalls, getCurrentCall, getCallsConfig, setMicPermissionsGranted} = require('@calls/state');
|
||||
getCallsState.mockReturnValue({
|
||||
enabled: {
|
||||
'join-channel': true,
|
||||
},
|
||||
});
|
||||
getChannelsWithCalls.mockReturnValue({
|
||||
'leave-channel': 'call1',
|
||||
'join-channel': 'call2',
|
||||
});
|
||||
getCurrentCall.mockReturnValue({
|
||||
serverUrl: 'server1',
|
||||
channelId: 'leave-channel',
|
||||
});
|
||||
getCallsConfig.mockReturnValue({});
|
||||
setMicPermissionsGranted.mockReturnValue(true);
|
||||
|
||||
const mockAlert = jest.spyOn(Alert, 'alert').mockImplementationOnce(async (title, message, buttons) => {
|
||||
// Simulate cancel
|
||||
await buttons![0].onPress?.();
|
||||
});
|
||||
|
||||
expect(await leaveAndJoinWithAlert(intl as any, 'server1', 'join-channel')).toBe(false);
|
||||
|
||||
console.log('rar');
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Are you sure you want to switch to a different call?',
|
||||
'You are already on a channel call in ~leave-channel. Do you want to leave your current call and join the call in ~join-channel?',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
onPress: expect.any(Function),
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: 'Leave & Join',
|
||||
onPress: expect.any(Function),
|
||||
isPreferred: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('shows leave and start confirmation when starting new call', async () => {
|
||||
const {getCallsState, getChannelsWithCalls, getCurrentCall, getCallsConfig, setMicPermissionsGranted} = require('@calls/state');
|
||||
getCallsState.mockReturnValue({
|
||||
enabled: {
|
||||
'join-channel': true,
|
||||
},
|
||||
});
|
||||
getChannelsWithCalls.mockReturnValue({
|
||||
'leave-channel': 'call1',
|
||||
});
|
||||
getCurrentCall.mockReturnValue({
|
||||
serverUrl: 'server1',
|
||||
channelId: 'leave-channel',
|
||||
});
|
||||
getCallsConfig.mockReturnValue({});
|
||||
setMicPermissionsGranted.mockReturnValue(true);
|
||||
|
||||
const mockAlert = jest.spyOn(Alert, 'alert').mockImplementationOnce((title, message, buttons) => {
|
||||
// Simulate confirm
|
||||
buttons![1].onPress?.();
|
||||
});
|
||||
|
||||
expect(await leaveAndJoinWithAlert(intl as any, 'server1', 'join-channel')).toBe(true);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Are you sure you want to switch to a different call?',
|
||||
'You are already on a channel call in ~leave-channel. Do you want to leave your current call and start a new call in ~join-channel?',
|
||||
expect.any(Array),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endCallConfirmationAlert', () => {
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}) => defaultMessage,
|
||||
};
|
||||
|
||||
it('shows end call confirmation for non-host', async () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
const promise = endCallConfirmationAlert(intl as any, false);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Are you sure you want to leave this call?',
|
||||
'',
|
||||
expect.any(Array),
|
||||
);
|
||||
|
||||
// Simulate leave
|
||||
const leaveButton = mockAlert.mock.calls[0][2]![1];
|
||||
leaveButton.onPress?.();
|
||||
expect(await promise).toBe(EndCallReturn.LeaveCall);
|
||||
});
|
||||
|
||||
it('shows end call confirmation for host', async () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
const promise = endCallConfirmationAlert(intl as any, true);
|
||||
|
||||
const buttons = mockAlert.mock.calls[0][2]!;
|
||||
expect(buttons).toHaveLength(3);
|
||||
expect(buttons.map((b) => b.text)).toEqual(['Cancel', 'End call for everyone', 'Leave call']);
|
||||
|
||||
// Simulate end call
|
||||
const endButton = buttons[1];
|
||||
endButton.onPress?.();
|
||||
expect(await promise).toBe(EndCallReturn.EndCall);
|
||||
});
|
||||
});
|
||||
});
|
||||
51
app/products/calls/calls_manager.test.ts
Normal file
51
app/products/calls/calls_manager.test.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AppState, Platform} from 'react-native';
|
||||
|
||||
import {callsOnAppStateChange} from '@calls/state';
|
||||
|
||||
import {CallsManager} from './calls_manager';
|
||||
|
||||
jest.mock('@calls/state', () => ({
|
||||
callsOnAppStateChange: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CallsManager', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(AppState, 'addEventListener');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset platform to its original value
|
||||
Platform.OS = 'ios';
|
||||
});
|
||||
|
||||
it('initializes AppState listeners correctly for iOS', () => {
|
||||
Platform.OS = 'ios';
|
||||
CallsManager.initialize();
|
||||
|
||||
expect(AppState.addEventListener).toHaveBeenCalledWith('change', callsOnAppStateChange);
|
||||
expect(AppState.addEventListener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('initializes AppState listeners correctly for Android', () => {
|
||||
Platform.OS = 'android';
|
||||
CallsManager.initialize();
|
||||
|
||||
expect(AppState.addEventListener).toHaveBeenCalledWith('blur', expect.any(Function));
|
||||
expect(AppState.addEventListener).toHaveBeenCalledWith('focus', expect.any(Function));
|
||||
expect(AppState.addEventListener).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Test that the blur callback calls callsOnAppStateChange with 'inactive'
|
||||
const blurCallback = jest.mocked(AppState.addEventListener).mock.calls[0][1];
|
||||
blurCallback('inactive');
|
||||
expect(callsOnAppStateChange).toHaveBeenCalledWith('inactive');
|
||||
|
||||
// Test that the focus callback calls callsOnAppStateChange with 'active'
|
||||
const focusCallback = jest.mocked(AppState.addEventListener).mock.calls[1][1];
|
||||
focusCallback('active');
|
||||
expect(callsOnAppStateChange).toHaveBeenCalledWith('active');
|
||||
});
|
||||
});
|
||||
221
app/products/calls/client/rest.test.ts
Normal file
221
app/products/calls/client/rest.test.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import clientCalls from './rest';
|
||||
|
||||
describe('ClientCalls', () => {
|
||||
const mockDoFetch = jest.fn();
|
||||
const mockGetCallsRoute = jest.fn(() => '/plugins/com.plugins.calls');
|
||||
|
||||
const BaseClass = class {
|
||||
doFetch = mockDoFetch;
|
||||
getCallsRoute = mockGetCallsRoute;
|
||||
};
|
||||
const Client = clientCalls(BaseClass);
|
||||
const client = new Client();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getEnabled', () => {
|
||||
it('returns true when version check succeeds', async () => {
|
||||
mockDoFetch.mockResolvedValueOnce({});
|
||||
const result = await client.getEnabled();
|
||||
expect(result).toBe(true);
|
||||
expect(mockDoFetch).toHaveBeenCalledWith('/plugins/com.plugins.calls/version', {method: 'get'});
|
||||
});
|
||||
|
||||
it('returns false when version check fails', async () => {
|
||||
mockDoFetch.mockRejectedValueOnce(new Error());
|
||||
const result = await client.getEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCalls', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.getCalls();
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/channels?mobilev2=true',
|
||||
{method: 'get', groupLabel: undefined},
|
||||
);
|
||||
});
|
||||
|
||||
it('passes groupLabel when provided', async () => {
|
||||
await client.getCalls('Server Switch');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/channels?mobilev2=true',
|
||||
{method: 'get', groupLabel: 'Server Switch'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCallForChannel', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.getCallForChannel('channel-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/channel-id?mobilev2=true',
|
||||
{method: 'get'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableChannelCalls', () => {
|
||||
it('makes correct API call to enable', async () => {
|
||||
await client.enableChannelCalls('channel-id', true);
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/channel-id',
|
||||
{method: 'post', body: {enabled: true}},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call to disable', async () => {
|
||||
await client.enableChannelCalls('channel-id', false);
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/channel-id',
|
||||
{method: 'post', body: {enabled: false}},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endCall', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.endCall('channel-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/channel-id/end',
|
||||
{method: 'post'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('host actions', () => {
|
||||
it('makes correct API call for hostMake', async () => {
|
||||
await client.hostMake('call-id', 'new-host-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/host/make',
|
||||
{method: 'post', body: {new_host_id: 'new-host-id'}},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call for hostMute', async () => {
|
||||
await client.hostMute('call-id', 'session-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/host/mute',
|
||||
{method: 'post', body: {session_id: 'session-id'}},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call for hostMuteOthers', async () => {
|
||||
await client.hostMuteOthers('call-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/host/mute-others',
|
||||
{method: 'post'},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call for hostScreenOff', async () => {
|
||||
await client.hostScreenOff('call-id', 'session-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/host/screen-off',
|
||||
{method: 'post', body: {session_id: 'session-id'}},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call for hostLowerHand', async () => {
|
||||
await client.hostLowerHand('call-id', 'session-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/host/lower-hand',
|
||||
{method: 'post', body: {session_id: 'session-id'}},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call for hostRemove', async () => {
|
||||
await client.hostRemove('call-id', 'session-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/host/remove',
|
||||
{method: 'post', body: {session_id: 'session-id'}},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCallsConfig', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.getCallsConfig();
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/config',
|
||||
{method: 'get', groupLabel: undefined},
|
||||
);
|
||||
});
|
||||
|
||||
it('passes groupLabel when provided', async () => {
|
||||
await client.getCallsConfig('Server Switch');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/config',
|
||||
{method: 'get', groupLabel: 'Server Switch'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersion', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.getVersion();
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/version',
|
||||
{method: 'get', groupLabel: undefined},
|
||||
);
|
||||
});
|
||||
|
||||
it('passes groupLabel when provided', async () => {
|
||||
await client.getVersion('Server Switch');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/version',
|
||||
{method: 'get', groupLabel: 'Server Switch'},
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty object on error', async () => {
|
||||
mockDoFetch.mockRejectedValueOnce(new Error());
|
||||
const result = await client.getVersion();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('genTURNCredentials', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.genTURNCredentials();
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/turn-credentials',
|
||||
{method: 'get'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recording actions', () => {
|
||||
it('makes correct API call for startCallRecording', async () => {
|
||||
await client.startCallRecording('call-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/recording/start',
|
||||
{method: 'post'},
|
||||
);
|
||||
});
|
||||
|
||||
it('makes correct API call for stopCallRecording', async () => {
|
||||
await client.stopCallRecording('call-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/call-id/recording/stop',
|
||||
{method: 'post'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissCall', () => {
|
||||
it('makes correct API call', async () => {
|
||||
await client.dismissCall('channel-id');
|
||||
expect(mockDoFetch).toHaveBeenCalledWith(
|
||||
'/plugins/com.plugins.calls/calls/channel-id/dismiss-notification',
|
||||
{method: 'post'},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -61,7 +61,7 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
|||
|
||||
getVersion = async (groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
return this.doFetch(
|
||||
return await this.doFetch(
|
||||
`${this.getCallsRoute()}/version`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,54 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {RTCMonitor, RTCPeer, parseRTCStats} from '@mattermost/calls/lib';
|
||||
import {zlibSync, strToU8} from 'fflate';
|
||||
import {Platform} from 'react-native';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import {newConnection} from './connection';
|
||||
import {WebSocketClient} from './websocket_client';
|
||||
import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
|
||||
|
||||
jest.mock('./websocket_client');
|
||||
jest.mock('@mattermost/calls/lib');
|
||||
jest.mock('react-native-webrtc', () => ({
|
||||
registerGlobals: jest.fn(),
|
||||
mediaDevices: {
|
||||
getUserMedia: jest.fn().mockResolvedValue({
|
||||
getAudioTracks: () => [{
|
||||
id: 'audioTrackId',
|
||||
enabled: true,
|
||||
}],
|
||||
getTracks: () => [{
|
||||
id: 'audioTrackId',
|
||||
enabled: true,
|
||||
stop: jest.fn(),
|
||||
release: jest.fn(),
|
||||
}],
|
||||
}),
|
||||
},
|
||||
}));
|
||||
jest.mock('@calls/connection/foreground_service', () => ({
|
||||
foregroundServiceStart: jest.fn(),
|
||||
foregroundServiceStop: jest.fn(),
|
||||
foregroundServiceSetup: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('newConnection', () => {
|
||||
const mockClient = {
|
||||
getWebSocketUrl: jest.fn(() => 'ws://localhost:8065'),
|
||||
getCallsConfig: jest.fn(() => ({})),
|
||||
getCallsConfig: jest.fn(() => ({
|
||||
ICEServers: ['stun:stun.example.com'],
|
||||
ICEServersConfigs: [{urls: ['stun:stun.example.com']}],
|
||||
AllowEnableCalls: true,
|
||||
EnableAV1: true,
|
||||
})),
|
||||
genTURNCredentials: jest.fn(() => Promise.resolve([{
|
||||
urls: ['turn:turn.example.com'],
|
||||
username: 'user',
|
||||
credential: 'pass',
|
||||
}])),
|
||||
};
|
||||
|
||||
const mockRTCStats = {
|
||||
|
|
@ -65,12 +98,16 @@ describe('newConnection', () => {
|
|||
RTCMonitor.mockImplementation(() => {
|
||||
return {
|
||||
on: jest.fn(),
|
||||
clearCache: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
start: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
Platform.OS = 'web';
|
||||
Platform.OS = 'android';
|
||||
|
||||
InCallManager.start = jest.fn();
|
||||
InCallManager.stop = jest.fn();
|
||||
InCallManager.stopProximitySensor = jest.fn();
|
||||
});
|
||||
|
||||
|
|
@ -82,7 +119,384 @@ describe('newConnection', () => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('join', async () => {
|
||||
const wsSend = jest.fn();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
RTCPeer.mockImplementation(() => ({
|
||||
on: jest.fn(),
|
||||
getStats: jest.fn(),
|
||||
}));
|
||||
|
||||
let openHandler;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'join') {
|
||||
handler();
|
||||
} else if (event === 'open') {
|
||||
openHandler = handler;
|
||||
handler();
|
||||
}
|
||||
},
|
||||
send: wsSend,
|
||||
}));
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
expect(connection).toBeDefined();
|
||||
|
||||
expect(wsSend).toHaveBeenCalledWith('join', {av1Support: false, channelID: 'channelID'});
|
||||
|
||||
// reconnect
|
||||
expect(openHandler).toBeDefined();
|
||||
openHandler!('originalConnID', 'prevConnID', true);
|
||||
expect(wsSend).toHaveBeenCalledWith('reconnect', {channelID: 'channelID', originalConnID: 'originalConnID', prevConnID: 'prevConnID'});
|
||||
});
|
||||
|
||||
it('mute/unmute', async () => {
|
||||
const mockReplaceTrack = jest.fn();
|
||||
const mockAddStream = jest.fn();
|
||||
const wsSend = jest.fn();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
RTCPeer.mockImplementation(() => ({
|
||||
replaceTrack: mockReplaceTrack,
|
||||
addStream: mockAddStream,
|
||||
on: jest.fn(),
|
||||
getStats: jest.fn(),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'join') {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
send: wsSend,
|
||||
}));
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
|
||||
// First unmute should use addStream
|
||||
connection.unmute();
|
||||
expect(mockAddStream).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(wsSend).toHaveBeenCalledWith('unmute');
|
||||
|
||||
connection.mute();
|
||||
|
||||
// Subsequent unmute should use replaceTrack
|
||||
connection.unmute();
|
||||
expect(mockReplaceTrack).toHaveBeenCalledWith(expect.any(String), expect.any(Object));
|
||||
expect(wsSend).toHaveBeenCalledWith('unmute');
|
||||
});
|
||||
|
||||
it('raise/unraise hand', async () => {
|
||||
const wsSend = jest.fn();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: jest.fn(),
|
||||
send: wsSend,
|
||||
}));
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
false,
|
||||
);
|
||||
|
||||
connection.raiseHand();
|
||||
expect(wsSend).toHaveBeenCalledWith('raise_hand');
|
||||
|
||||
connection.unraiseHand();
|
||||
expect(wsSend).toHaveBeenCalledWith('unraise_hand');
|
||||
});
|
||||
|
||||
it('react', async () => {
|
||||
const wsSend = jest.fn();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: jest.fn(),
|
||||
send: wsSend,
|
||||
}));
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
false,
|
||||
);
|
||||
|
||||
const emoji = {name: 'thumbsup', unified: '1F44D'};
|
||||
connection.sendReaction(emoji);
|
||||
expect(wsSend).toHaveBeenCalledWith('react', {
|
||||
data: JSON.stringify(emoji),
|
||||
});
|
||||
});
|
||||
|
||||
it('ws error', async () => {
|
||||
const mockCloseCb = jest.fn();
|
||||
let errorHandler: (err: Error) => void;
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'error') {
|
||||
errorHandler = handler;
|
||||
}
|
||||
},
|
||||
send: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
mockCloseCb,
|
||||
() => {},
|
||||
false,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
errorHandler(new Error('test error'));
|
||||
expect(mockCloseCb).not.toHaveBeenCalled();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
errorHandler(wsReconnectionTimeoutErr);
|
||||
expect(mockCloseCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ws close', async () => {
|
||||
let closeHandler: (event: WebSocketCloseEvent) => void;
|
||||
const mockCloseCb = jest.fn();
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'close') {
|
||||
closeHandler = handler;
|
||||
}
|
||||
},
|
||||
send: jest.fn(),
|
||||
}));
|
||||
|
||||
await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
mockCloseCb,
|
||||
() => {},
|
||||
false,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
closeHandler({code: 1000, reason: 'normal'});
|
||||
expect(mockCloseCb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('voice track', async () => {
|
||||
const getUserMedia = require('react-native-webrtc').mediaDevices.getUserMedia;
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(getUserMedia).toHaveBeenCalledWith({
|
||||
video: false,
|
||||
audio: true,
|
||||
});
|
||||
expect(getUserMedia).toHaveBeenCalledTimes(1);
|
||||
|
||||
await connection.initializeVoiceTrack();
|
||||
expect(getUserMedia).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rtc peer', async () => {
|
||||
const wsSend = jest.fn();
|
||||
const wsClose = jest.fn();
|
||||
const peerDestroy = jest.fn();
|
||||
const peerSignal = jest.fn();
|
||||
|
||||
const handlers: Record<string, any> = {};
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
RTCPeer.mockImplementation(() => ({
|
||||
on: (event: string, handler: any) => {
|
||||
handlers[event] = handler;
|
||||
},
|
||||
getStats: jest.fn(),
|
||||
destroy: peerDestroy,
|
||||
signal: peerSignal,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
parseRTCStats.mockImplementation(() => mockRTCStats);
|
||||
|
||||
let wsMsgHandler;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'join') {
|
||||
handler();
|
||||
} else if (event === 'message') {
|
||||
wsMsgHandler = handler;
|
||||
}
|
||||
},
|
||||
send: wsSend,
|
||||
close: wsClose,
|
||||
}));
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
expect(connection).toBeDefined();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
handlers.candidate({candidate: 'candidate'});
|
||||
expect(wsSend).toHaveBeenCalledWith('ice', {data: '{"candidate":"candidate"}'});
|
||||
|
||||
handlers.offer({offer: 'sdp'});
|
||||
expect(wsSend).toHaveBeenCalledWith('sdp', {data: zlibSync(strToU8('{"offer":"sdp"}'))}, true);
|
||||
|
||||
expect(wsMsgHandler).toBeDefined();
|
||||
|
||||
wsMsgHandler!({data: '{"type": "answer"}'});
|
||||
expect(peerSignal).toHaveBeenCalledWith('{"type": "answer"}');
|
||||
|
||||
wsMsgHandler!({data: '{"type": "offer"}'});
|
||||
expect(peerSignal).toHaveBeenCalledWith('{"type": "offer"}');
|
||||
|
||||
wsMsgHandler!({data: '{"type": "candidate"}'});
|
||||
expect(peerSignal).toHaveBeenCalledWith('{"type": "candidate"}');
|
||||
|
||||
const mockGetTracks = jest.fn().mockReturnValue([
|
||||
{
|
||||
id: 'audioTrackId', enabled: true, stop: jest.fn(), release: jest.fn(),
|
||||
},
|
||||
]);
|
||||
const mockGetVideoTracks = jest.fn().mockReturnValue([]);
|
||||
handlers.stream({
|
||||
getTracks: mockGetTracks,
|
||||
getVideoTracks: mockGetVideoTracks,
|
||||
});
|
||||
expect(mockGetTracks).toHaveBeenCalled();
|
||||
expect(mockGetVideoTracks).toHaveBeenCalled();
|
||||
|
||||
handlers.error(new Error('test error'));
|
||||
expect(wsClose).toHaveBeenCalled();
|
||||
expect(wsSend).toHaveBeenCalledWith('leave');
|
||||
expect(peerDestroy).toHaveBeenCalled();
|
||||
|
||||
handlers.close();
|
||||
});
|
||||
|
||||
it('rtc peer close', async () => {
|
||||
const wsSend = jest.fn();
|
||||
const wsClose = jest.fn();
|
||||
const peerDestroy = jest.fn();
|
||||
const peerSignal = jest.fn();
|
||||
|
||||
const handlers: Record<string, any> = {};
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
RTCPeer.mockImplementation(() => ({
|
||||
on: (event: string, handler: any) => {
|
||||
handlers[event] = handler;
|
||||
},
|
||||
getStats: jest.fn(),
|
||||
destroy: peerDestroy,
|
||||
signal: peerSignal,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
parseRTCStats.mockImplementation(() => mockRTCStats);
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'join') {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
send: wsSend,
|
||||
close: wsClose,
|
||||
}));
|
||||
|
||||
const connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
expect(connection).toBeDefined();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
handlers.close();
|
||||
|
||||
expect(wsClose).toHaveBeenCalled();
|
||||
expect(wsSend).toHaveBeenCalledWith('leave');
|
||||
expect(peerDestroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('collectICEStats', (done) => {
|
||||
Platform.OS = 'web';
|
||||
|
||||
const wsSend = jest.fn();
|
||||
const joinHandler = jest.fn(async (event: string, cb: () => void) => {
|
||||
if (event === 'join') {
|
||||
|
|
@ -127,4 +541,63 @@ describe('newConnection', () => {
|
|||
expect(joinHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('waitForPeerConnection', async () => {
|
||||
let connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
expect(connection).toBeDefined();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
let res = connection.waitForPeerConnection();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await expect(res).rejects.toEqual('timed out waiting for peer connection');
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
RTCPeer.mockImplementation(() => {
|
||||
return {
|
||||
getStats: jest.fn(),
|
||||
on: jest.fn(),
|
||||
connected: true,
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
WebSocketClient.mockImplementation(() => ({
|
||||
initialize: jest.fn(),
|
||||
on: (event: string, handler: any) => {
|
||||
if (event === 'join') {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
send: jest.fn(),
|
||||
}));
|
||||
|
||||
connection = await newConnection(
|
||||
'http://localhost:8065',
|
||||
'channelID',
|
||||
() => {},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
expect(connection).toBeDefined();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
res = connection.waitForPeerConnection();
|
||||
jest.runAllTimers();
|
||||
await expect(res).resolves.not.toThrow();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
273
app/products/calls/connection/websocket_client.test.ts
Normal file
273
app/products/calls/connection/websocket_client.test.ts
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// At the moment it feels to much work to make this test pass all the typescript rules due to the heavy mocking involved (e.g., WebSocket global).
|
||||
// eslint-disable-next-line
|
||||
// @ts-nocheck
|
||||
|
||||
import {encode} from '@msgpack/msgpack/dist';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
||||
import {WebSocketClient, wsReconnectionTimeoutErr, wsReconnectionTimeout} from './websocket_client';
|
||||
|
||||
describe('WebSocketClient', () => {
|
||||
const mockServerUrl = 'server1';
|
||||
const mockWsPath = '/api/v4/websocket';
|
||||
const mockToken = 'mock-token';
|
||||
|
||||
beforeAll(async () => {
|
||||
global.WebSocket = jest.fn();
|
||||
global.WebSocket.CONNECTING = 0;
|
||||
global.WebSocket.OPEN = 1;
|
||||
global.WebSocket.CLOSING = 2;
|
||||
global.WebSocket.CLOSED = 3;
|
||||
|
||||
await DatabaseManager.init(['server1']);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete global.WebSocket;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('init', async () => {
|
||||
let ws = new WebSocketClient('server2', mockWsPath, mockToken);
|
||||
await ws.initialize();
|
||||
expect(ws.ws).toBeNull();
|
||||
|
||||
ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
expect(ws).toBeDefined();
|
||||
expect(ws.ws).toBeNull();
|
||||
|
||||
await ws.init(false);
|
||||
expect(ws.ws).not.toBeNull();
|
||||
|
||||
expect(ws.ws.onopen).toBeUndefined();
|
||||
expect(ws.ws.onerror).toBeDefined();
|
||||
expect(ws.ws.onclose).toBeDefined();
|
||||
});
|
||||
|
||||
it('onerror', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
await ws.init(false);
|
||||
|
||||
const err = new Error('test error');
|
||||
|
||||
ws.emit = jest.fn();
|
||||
|
||||
ws.ws.onerror(err);
|
||||
|
||||
expect(ws.emit).toHaveBeenCalledWith('error', err);
|
||||
});
|
||||
|
||||
it('onclose', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
ws.originalConnID = 'originalConnID';
|
||||
ws.connID = 'connID';
|
||||
await ws.init(false);
|
||||
|
||||
ws.emit = jest.fn();
|
||||
|
||||
jest.spyOn(ws, 'init');
|
||||
jest.spyOn(ws, 'reconnect');
|
||||
|
||||
// Test closed case
|
||||
ws.closed = true;
|
||||
|
||||
const ev = {code: 1000, reason: 'test reason'};
|
||||
ws.ws.onclose(ev);
|
||||
expect(ws.emit).toHaveBeenCalledWith('close', ev);
|
||||
expect(ws.reconnect).not.toHaveBeenCalled();
|
||||
|
||||
// Test reconnect case
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(global, 'setTimeout');
|
||||
|
||||
ws.closed = false;
|
||||
ws.ws.onclose(ev);
|
||||
expect(ws.emit).toHaveBeenCalledWith('close', ev);
|
||||
expect(ws.reconnect).toHaveBeenCalled();
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
await expect(ws.init).toHaveBeenCalledWith(true);
|
||||
expect(ws.ws.onopen).toBeDefined();
|
||||
|
||||
ws.ws.onopen();
|
||||
expect(ws.emit).toHaveBeenCalledWith('open', 'originalConnID', 'connID', true);
|
||||
});
|
||||
|
||||
it('state', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
expect(ws.state()).toBe(WebSocket.CLOSED);
|
||||
|
||||
await ws.init(false);
|
||||
ws.ws.readyState = WebSocket.CONNECTING;
|
||||
expect(ws.state()).toBe(WebSocket.CONNECTING);
|
||||
});
|
||||
|
||||
it('close', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
await ws.init(false);
|
||||
|
||||
const close = jest.fn();
|
||||
ws.ws.close = close;
|
||||
|
||||
ws.close();
|
||||
expect(ws.closed).toBe(true);
|
||||
expect(close).toHaveBeenCalled();
|
||||
expect(ws.ws).toBeNull();
|
||||
expect(ws.seqNo).toBe(1);
|
||||
expect(ws.serverSeqNo).toBe(0);
|
||||
expect(ws.connID).toBe('');
|
||||
expect(ws.originalConnID).toBe('');
|
||||
});
|
||||
|
||||
it('reconnection timeout', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
await ws.init(false);
|
||||
|
||||
// Mock error handler
|
||||
const errorHandler = jest.fn();
|
||||
ws.on('error', errorHandler);
|
||||
|
||||
// Set last disconnect to be longer than timeout
|
||||
ws.lastDisconnect = Date.now() - (wsReconnectionTimeout + 1000);
|
||||
ws.reconnect();
|
||||
|
||||
expect(errorHandler).toHaveBeenCalledWith(wsReconnectionTimeoutErr);
|
||||
expect(ws.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('onmessage', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
await ws.init(false);
|
||||
|
||||
// Test hello message
|
||||
const helloMsg = {
|
||||
event: 'hello',
|
||||
data: {
|
||||
connection_id: 'new_conn_id',
|
||||
},
|
||||
};
|
||||
ws.ws.onmessage({data: JSON.stringify(helloMsg)});
|
||||
expect(ws.connID).toBe('new_conn_id');
|
||||
expect(ws.serverSeqNo).toBe(0);
|
||||
|
||||
// Test signal message
|
||||
const joinHandler = jest.fn();
|
||||
ws.on('join', joinHandler);
|
||||
const joinMsg = {
|
||||
event: 'custom_com.mattermost.calls_join',
|
||||
seq: 1,
|
||||
data: {
|
||||
connID: 'new_conn_id',
|
||||
},
|
||||
};
|
||||
ws.ws.onmessage({data: JSON.stringify(joinMsg)});
|
||||
expect(joinHandler).toHaveBeenCalled();
|
||||
expect(ws.serverSeqNo).toBe(2);
|
||||
|
||||
// Test error message
|
||||
const errorHandler = jest.fn();
|
||||
ws.on('error', errorHandler);
|
||||
const errorMsg = {
|
||||
event: 'custom_com.mattermost.calls_error',
|
||||
seq: 2,
|
||||
data: {
|
||||
connID: 'new_conn_id',
|
||||
error: 'test error',
|
||||
},
|
||||
};
|
||||
ws.ws.onmessage({data: JSON.stringify(errorMsg)});
|
||||
expect(errorHandler).toHaveBeenCalledWith(errorMsg.data);
|
||||
expect(ws.serverSeqNo).toBe(3);
|
||||
|
||||
// Test signal message
|
||||
const messageHandler = jest.fn();
|
||||
ws.on('message', messageHandler);
|
||||
const signalMsg = {
|
||||
event: 'custom_com.mattermost.calls_signal',
|
||||
seq: 3,
|
||||
data: {
|
||||
connID: 'new_conn_id',
|
||||
signal: 'test signal',
|
||||
},
|
||||
};
|
||||
ws.ws.onmessage({data: JSON.stringify(signalMsg)});
|
||||
expect(messageHandler).toHaveBeenCalledWith(signalMsg.data);
|
||||
expect(ws.serverSeqNo).toBe(4);
|
||||
|
||||
// Test invalid message
|
||||
ws.ws.onmessage({data: 'invalid json'});
|
||||
expect(ws.serverSeqNo).toBe(4);
|
||||
|
||||
// Test message without event
|
||||
ws.ws.onmessage({data: JSON.stringify({seq: 4})});
|
||||
expect(ws.serverSeqNo).toBe(5);
|
||||
|
||||
// Test message with wrong connID
|
||||
const wrongConnIDMsg = {
|
||||
event: 'custom_com.mattermost.calls_signal',
|
||||
seq: 5,
|
||||
data: {
|
||||
connID: 'wrong_conn_id',
|
||||
signal: 'test signal',
|
||||
},
|
||||
};
|
||||
ws.ws.onmessage({data: JSON.stringify(wrongConnIDMsg)});
|
||||
expect(messageHandler).toHaveBeenCalledTimes(1);
|
||||
expect(ws.serverSeqNo).toBe(6);
|
||||
});
|
||||
|
||||
it('send', async () => {
|
||||
const ws = new WebSocketClient(mockServerUrl, mockWsPath, mockToken);
|
||||
await ws.init(false);
|
||||
|
||||
// Mock the WebSocket send method
|
||||
ws.ws.send = jest.fn();
|
||||
|
||||
// Test JSON message
|
||||
const action = 'test_action';
|
||||
const data = {foo: 'bar'};
|
||||
ws.ws.readyState = WebSocket.OPEN;
|
||||
ws.send(action, data);
|
||||
expect(ws.ws.send).toHaveBeenCalledWith(JSON.stringify({
|
||||
action: `custom_com.mattermost.calls_${action}`,
|
||||
seq: 1,
|
||||
data,
|
||||
}));
|
||||
expect(ws.ws.send).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test binary message
|
||||
ws.send(action, data, true);
|
||||
expect(ws.ws.send).toHaveBeenCalledWith(encode({
|
||||
action: `custom_com.mattermost.calls_${action}`,
|
||||
seq: 2,
|
||||
data,
|
||||
}));
|
||||
expect(ws.ws.send).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Test when WebSocket is not ready
|
||||
ws.ws.readyState = WebSocket.CONNECTING;
|
||||
ws.send(action, data);
|
||||
expect(ws.ws.send).toHaveBeenCalledTimes(2); // No additional call
|
||||
|
||||
// Test when WebSocket is closed
|
||||
ws.ws.readyState = WebSocket.CLOSED;
|
||||
ws.send(action, data);
|
||||
expect(ws.ws.send).toHaveBeenCalledTimes(2); // No additional call
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@ import {getConfigValue} from '@queries/servers/system';
|
|||
import {logError, logDebug} from '@utils/log';
|
||||
|
||||
const wsMinReconnectRetryTimeMs = 1000; // 1 second
|
||||
const wsReconnectionTimeout = 30000; // 30 seconds
|
||||
export const wsReconnectionTimeout = 30000; // 30 seconds
|
||||
const wsReconnectTimeIncrement = 500; // 0.5 seconds
|
||||
export const wsReconnectionTimeoutErr = new Error('max disconnected time reached');
|
||||
|
||||
|
|
|
|||
581
app/products/calls/connection/websocket_event_handlers.test.ts
Normal file
581
app/products/calls/connection/websocket_event_handlers.test.ts
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {fetchUsersByIds} from '@actions/remote/user';
|
||||
import {leaveCall, muteMyself, unraiseHand} from '@calls/actions';
|
||||
import {createCallAndAddToIds} from '@calls/actions/calls';
|
||||
import {hostRemovedErr} from '@calls/errors';
|
||||
import {
|
||||
callEnded,
|
||||
callStarted,
|
||||
getCallsConfig,
|
||||
getCurrentCall,
|
||||
receivedCaption,
|
||||
removeIncomingCall,
|
||||
setCallForChannel,
|
||||
setCallScreenOff,
|
||||
setCallScreenOn,
|
||||
setCaptioningState,
|
||||
setChannelEnabled,
|
||||
setHost,
|
||||
setRaisedHand,
|
||||
setRecordingState,
|
||||
setUserMuted,
|
||||
setUserVoiceOn,
|
||||
userJoinedCall,
|
||||
userLeftCall,
|
||||
userReacted,
|
||||
} from '@calls/state';
|
||||
import {type HostControlsLowerHandMsgData, type HostControlsMsgData, DefaultCallsConfig, DefaultCurrentCall, DefaultCall} from '@calls/types/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import {
|
||||
handleCallCaption,
|
||||
handleCallChannelDisabled,
|
||||
handleCallChannelEnabled,
|
||||
handleCallEnded,
|
||||
handleCallHostChanged,
|
||||
handleCallJobState,
|
||||
handleCallScreenOff,
|
||||
handleCallScreenOn,
|
||||
handleCallStarted,
|
||||
handleCallState,
|
||||
handleCallUserConnected,
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserJoined,
|
||||
handleCallUserLeft,
|
||||
handleCallUserMuted,
|
||||
handleCallUserRaiseHand,
|
||||
handleCallUserReacted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserUnraiseHand,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallUserVoiceOn,
|
||||
handleHostLowerHand,
|
||||
handleHostMute,
|
||||
handleHostRemoved,
|
||||
handleUserDismissedNotification,
|
||||
} from './websocket_event_handlers';
|
||||
|
||||
import type {
|
||||
CallHostChangedData,
|
||||
CallJobStateData,
|
||||
CallStartData,
|
||||
CallStateData,
|
||||
EmptyData,
|
||||
LiveCaptionData,
|
||||
UserConnectedData,
|
||||
UserDisconnectedData,
|
||||
UserDismissedNotification,
|
||||
UserJoinedData,
|
||||
UserLeftData,
|
||||
UserMutedUnmutedData,
|
||||
UserRaiseUnraiseHandData,
|
||||
UserReactionData,
|
||||
UserScreenOnOffData,
|
||||
UserVoiceOnOffData,
|
||||
} from '@mattermost/calls/lib/types';
|
||||
|
||||
jest.mock('@actions/remote/user');
|
||||
jest.mock('@calls/actions/calls');
|
||||
jest.mock('@calls/state');
|
||||
jest.mock('@database/manager');
|
||||
jest.mock('@queries/servers/system');
|
||||
|
||||
describe('websocket event handlers', () => {
|
||||
const serverUrl = 'server1';
|
||||
const channelId = 'channel-id';
|
||||
const userId = 'user-id';
|
||||
const sessionId = 'session-id';
|
||||
|
||||
beforeAll(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.21.0'},
|
||||
});
|
||||
jest.mocked(getCurrentCall).mockReturnValue({
|
||||
...DefaultCurrentCall,
|
||||
serverUrl,
|
||||
channelId,
|
||||
mySessionId: sessionId,
|
||||
id: 'call-id',
|
||||
startTime: Date.now(),
|
||||
ownerId: 'owner-id',
|
||||
hostId: 'host-id',
|
||||
threadId: 'thread-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallUserConnected/Disconnected (deprecated)', () => {
|
||||
it('should handle user connected for old version', () => {
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.20.0'},
|
||||
});
|
||||
handleCallUserConnected(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {userID: userId},
|
||||
} as WebSocketMessage<UserConnectedData>);
|
||||
expect(fetchUsersByIds).toHaveBeenCalledWith(serverUrl, [userId]);
|
||||
expect(userJoinedCall).toHaveBeenCalledWith(serverUrl, channelId, userId, userId);
|
||||
});
|
||||
|
||||
it('should not handle user connected for new version', () => {
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.21.0'},
|
||||
});
|
||||
handleCallUserConnected(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {userID: userId},
|
||||
} as WebSocketMessage<UserConnectedData>);
|
||||
expect(fetchUsersByIds).not.toHaveBeenCalled();
|
||||
expect(userJoinedCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle user disconnected for old version', () => {
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.20.0'},
|
||||
});
|
||||
handleCallUserDisconnected(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {userID: userId},
|
||||
} as WebSocketMessage<UserDisconnectedData>);
|
||||
expect(userLeftCall).toHaveBeenCalledWith(serverUrl, channelId, userId);
|
||||
});
|
||||
|
||||
it('should not handle user disconnected for new version', () => {
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.21.0'},
|
||||
});
|
||||
handleCallUserDisconnected(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {userID: userId},
|
||||
} as WebSocketMessage<UserDisconnectedData>);
|
||||
expect(userLeftCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallUserJoined/Left', () => {
|
||||
it('should handle user joined', () => {
|
||||
handleCallUserJoined(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {user_id: userId, session_id: sessionId},
|
||||
} as WebSocketMessage<UserJoinedData>);
|
||||
expect(fetchUsersByIds).toHaveBeenCalledWith(serverUrl, [userId]);
|
||||
expect(userJoinedCall).toHaveBeenCalledWith(serverUrl, channelId, userId, sessionId);
|
||||
});
|
||||
|
||||
it('should handle user left', () => {
|
||||
handleCallUserLeft(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserLeftData>);
|
||||
expect(userLeftCall).toHaveBeenCalledWith(serverUrl, channelId, sessionId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallUserMuted/Unmuted', () => {
|
||||
it('should handle user muted', () => {
|
||||
handleCallUserMuted(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserMutedUnmutedData>);
|
||||
expect(setUserMuted).toHaveBeenCalledWith(serverUrl, channelId, sessionId, true);
|
||||
});
|
||||
|
||||
it('should handle user unmuted', () => {
|
||||
handleCallUserUnmuted(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserMutedUnmutedData>);
|
||||
expect(setUserMuted).toHaveBeenCalledWith(serverUrl, channelId, sessionId, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallUserVoiceOn/Off', () => {
|
||||
it('should handle voice on', () => {
|
||||
handleCallUserVoiceOn({
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserVoiceOnOffData>);
|
||||
expect(setUserVoiceOn).toHaveBeenCalledWith(channelId, sessionId, true);
|
||||
});
|
||||
|
||||
it('should handle voice off', () => {
|
||||
handleCallUserVoiceOff({
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserVoiceOnOffData>);
|
||||
expect(setUserVoiceOn).toHaveBeenCalledWith(channelId, sessionId, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallStarted/Ended', () => {
|
||||
it('should handle call started', () => {
|
||||
const startTime = Date.now();
|
||||
handleCallStarted(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {
|
||||
id: 'call-id',
|
||||
channelID: channelId,
|
||||
start_at: startTime,
|
||||
thread_id: 'thread-id',
|
||||
owner_id: 'owner-id',
|
||||
host_id: 'host-id',
|
||||
},
|
||||
} as WebSocketMessage<CallStartData>);
|
||||
expect(callStarted).toHaveBeenCalledWith(serverUrl, {
|
||||
id: 'call-id',
|
||||
channelId,
|
||||
startTime,
|
||||
threadId: 'thread-id',
|
||||
screenOn: '',
|
||||
sessions: {},
|
||||
ownerId: 'owner-id',
|
||||
hostId: 'host-id',
|
||||
dismissed: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle call ended', () => {
|
||||
jest.spyOn(DeviceEventEmitter, 'emit');
|
||||
handleCallEnded(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {},
|
||||
} as WebSocketMessage<EmptyData>);
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith('custom_com.mattermost.calls_call_end', {channelId});
|
||||
expect(callEnded).toHaveBeenCalledWith(serverUrl, channelId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallChannelEnabled/Disabled', () => {
|
||||
it('should handle channel enabled', () => {
|
||||
handleCallChannelEnabled(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {},
|
||||
} as WebSocketMessage<EmptyData>);
|
||||
expect(setChannelEnabled).toHaveBeenCalledWith(serverUrl, channelId, true);
|
||||
});
|
||||
|
||||
it('should handle channel disabled', () => {
|
||||
handleCallChannelDisabled(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {},
|
||||
} as WebSocketMessage<EmptyData>);
|
||||
expect(setChannelEnabled).toHaveBeenCalledWith(serverUrl, channelId, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallScreenOn/Off', () => {
|
||||
it('should handle screen on', () => {
|
||||
handleCallScreenOn(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserScreenOnOffData>);
|
||||
expect(setCallScreenOn).toHaveBeenCalledWith(serverUrl, channelId, sessionId);
|
||||
});
|
||||
|
||||
it('should handle screen off', () => {
|
||||
handleCallScreenOff(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId},
|
||||
} as WebSocketMessage<UserScreenOnOffData>);
|
||||
expect(setCallScreenOff).toHaveBeenCalledWith(serverUrl, channelId, sessionId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallUserRaiseHand/UnraiseHand', () => {
|
||||
it('should handle raise hand', () => {
|
||||
handleCallUserRaiseHand(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId, raised_hand: 12345},
|
||||
} as WebSocketMessage<UserRaiseUnraiseHandData>);
|
||||
expect(setRaisedHand).toHaveBeenCalledWith(serverUrl, channelId, sessionId, 12345);
|
||||
});
|
||||
|
||||
it('should handle unraise hand', () => {
|
||||
handleCallUserUnraiseHand(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {session_id: sessionId, raised_hand: 0},
|
||||
} as WebSocketMessage<UserRaiseUnraiseHandData>);
|
||||
expect(setRaisedHand).toHaveBeenCalledWith(serverUrl, channelId, sessionId, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallUserReacted', () => {
|
||||
it('should handle user reaction for old version', () => {
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.20.0'},
|
||||
});
|
||||
const reactionData = {
|
||||
user_id: userId,
|
||||
session_id: sessionId,
|
||||
reaction: '👍',
|
||||
timestamp: 12345,
|
||||
emoji: {name: 'thumbsup', unified: '1F44D'},
|
||||
};
|
||||
handleCallUserReacted(serverUrl, {
|
||||
event: 'custom_com.mattermost.calls_reaction',
|
||||
seq: 1,
|
||||
broadcast: {
|
||||
omit_users: {} as Dictionary<boolean>,
|
||||
user_id: '',
|
||||
team_id: '',
|
||||
channel_id: channelId,
|
||||
},
|
||||
data: reactionData,
|
||||
} as WebSocketMessage<UserReactionData>);
|
||||
expect(userReacted).toHaveBeenCalledWith(serverUrl, channelId, {
|
||||
...reactionData,
|
||||
session_id: userId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle user reaction for new version', () => {
|
||||
jest.mocked(getCallsConfig).mockReturnValue({
|
||||
...DefaultCallsConfig,
|
||||
version: {version: '0.21.0'},
|
||||
});
|
||||
const reactionData = {
|
||||
user_id: userId,
|
||||
session_id: sessionId,
|
||||
reaction: '👍',
|
||||
timestamp: 12345,
|
||||
emoji: {name: 'thumbsup', unified: '1F44D'},
|
||||
};
|
||||
handleCallUserReacted(serverUrl, {
|
||||
event: 'custom_com.mattermost.calls_reaction',
|
||||
seq: 1,
|
||||
broadcast: {
|
||||
omit_users: {} as Dictionary<boolean>,
|
||||
user_id: '',
|
||||
team_id: '',
|
||||
channel_id: channelId,
|
||||
},
|
||||
data: reactionData,
|
||||
} as WebSocketMessage<UserReactionData>);
|
||||
expect(userReacted).toHaveBeenCalledWith(serverUrl, channelId, reactionData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallHostChanged', () => {
|
||||
it('should handle host changed', () => {
|
||||
handleCallHostChanged(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {hostID: 'new-host-id'},
|
||||
} as WebSocketMessage<CallHostChangedData>);
|
||||
expect(setHost).toHaveBeenCalledWith(serverUrl, channelId, 'new-host-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleHostControls', () => {
|
||||
it('should handle host mute', () => {
|
||||
handleHostMute(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {channel_id: channelId, session_id: sessionId},
|
||||
} as WebSocketMessage<HostControlsMsgData>);
|
||||
expect(muteMyself).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not handle host mute for different server', () => {
|
||||
handleHostMute('different-server', {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {channel_id: channelId, session_id: sessionId},
|
||||
} as WebSocketMessage<HostControlsMsgData>);
|
||||
expect(muteMyself).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle host lower hand', () => {
|
||||
handleHostLowerHand(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {channel_id: channelId, session_id: sessionId},
|
||||
} as WebSocketMessage<HostControlsLowerHandMsgData>);
|
||||
expect(unraiseHand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not handle host lower hand for different server', () => {
|
||||
handleHostLowerHand('different-server', {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {channel_id: channelId, session_id: sessionId},
|
||||
} as WebSocketMessage<HostControlsLowerHandMsgData>);
|
||||
expect(unraiseHand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle host removed', () => {
|
||||
handleHostRemoved(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {channel_id: channelId, session_id: sessionId},
|
||||
} as WebSocketMessage<HostControlsMsgData>);
|
||||
expect(leaveCall).toHaveBeenCalledWith(hostRemovedErr);
|
||||
});
|
||||
|
||||
it('should not handle host removed for different server', () => {
|
||||
handleHostRemoved('different-server', {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {channel_id: channelId, session_id: sessionId},
|
||||
} as WebSocketMessage<HostControlsMsgData>);
|
||||
expect(leaveCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserDismissedNotification', () => {
|
||||
it('should handle user dismissed notification for current user', async () => {
|
||||
const myUserId = 'my-user-id';
|
||||
jest.mocked(getCurrentUserId).mockResolvedValue(myUserId);
|
||||
|
||||
await handleUserDismissedNotification(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {
|
||||
userID: myUserId,
|
||||
callID: 'call-id',
|
||||
},
|
||||
} as WebSocketMessage<UserDismissedNotification>);
|
||||
|
||||
expect(removeIncomingCall).toHaveBeenCalledWith(serverUrl, 'call-id');
|
||||
});
|
||||
|
||||
it('should not handle user dismissed notification for other users', async () => {
|
||||
const myUserId = 'my-user-id';
|
||||
jest.mocked(getCurrentUserId).mockResolvedValue(myUserId);
|
||||
|
||||
await handleUserDismissedNotification(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {
|
||||
userID: 'other-user-id',
|
||||
callID: 'call-id',
|
||||
},
|
||||
} as WebSocketMessage<UserDismissedNotification>);
|
||||
|
||||
expect(removeIncomingCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not handle user dismissed notification when database is unavailable', async () => {
|
||||
await handleUserDismissedNotification('server2', {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {
|
||||
userID: 'user-id',
|
||||
callID: 'call-id',
|
||||
},
|
||||
} as WebSocketMessage<UserDismissedNotification>);
|
||||
|
||||
expect(removeIncomingCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallJobState', () => {
|
||||
it('should handle recording job state', () => {
|
||||
const jobState = {
|
||||
type: 'recording',
|
||||
init_at: 12345,
|
||||
start_at: 12345,
|
||||
end_at: 0,
|
||||
};
|
||||
|
||||
handleCallJobState(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {jobState},
|
||||
} as WebSocketMessage<CallJobStateData>);
|
||||
|
||||
expect(setRecordingState).toHaveBeenCalledWith(serverUrl, channelId, jobState);
|
||||
expect(setCaptioningState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle captioning job state', () => {
|
||||
const jobState = {
|
||||
type: 'captioning',
|
||||
init_at: 12345,
|
||||
start_at: 12345,
|
||||
end_at: 0,
|
||||
};
|
||||
|
||||
handleCallJobState(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {jobState},
|
||||
} as WebSocketMessage<CallJobStateData>);
|
||||
|
||||
expect(setCaptioningState).toHaveBeenCalledWith(serverUrl, channelId, jobState);
|
||||
expect(setRecordingState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallCaption', () => {
|
||||
it('should handle call caption', () => {
|
||||
const captionData = {
|
||||
session_id: 'session-id',
|
||||
caption_id: 'caption-id',
|
||||
channel_id: channelId,
|
||||
user_id: 'user-id',
|
||||
text: 'Hello world',
|
||||
start_at: 12345,
|
||||
end_at: 12346,
|
||||
};
|
||||
|
||||
handleCallCaption(serverUrl, {
|
||||
event: 'custom_com.mattermost.calls_caption',
|
||||
seq: 1,
|
||||
broadcast: {
|
||||
omit_users: {} as Dictionary<boolean>,
|
||||
user_id: '',
|
||||
team_id: '',
|
||||
channel_id: channelId,
|
||||
},
|
||||
data: captionData,
|
||||
} as WebSocketMessage<LiveCaptionData>);
|
||||
|
||||
expect(receivedCaption).toHaveBeenCalledWith(serverUrl, captionData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallState', () => {
|
||||
it('should handle call state', () => {
|
||||
const callState = {
|
||||
...DefaultCall,
|
||||
id: 'call-id',
|
||||
channelId,
|
||||
startTime: Date.now(),
|
||||
screenOn: '',
|
||||
sessions: {},
|
||||
ownerId: 'owner-id',
|
||||
hostId: 'host-id',
|
||||
dismissed: {},
|
||||
recording: {
|
||||
init_at: 12345,
|
||||
start_at: 12345,
|
||||
end_at: 0,
|
||||
},
|
||||
live_captions: {
|
||||
init_at: 12345,
|
||||
start_at: 12345,
|
||||
end_at: 0,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mocked(createCallAndAddToIds).mockReturnValue(callState);
|
||||
|
||||
handleCallState(serverUrl, {
|
||||
broadcast: {channel_id: channelId},
|
||||
data: {
|
||||
channel_id: channelId,
|
||||
call: JSON.stringify(callState),
|
||||
},
|
||||
} as WebSocketMessage<CallStateData>);
|
||||
|
||||
expect(createCallAndAddToIds).toHaveBeenCalledWith(channelId, callState);
|
||||
expect(setCallForChannel).toHaveBeenCalledWith(serverUrl, channelId, callState);
|
||||
expect(setRecordingState).toHaveBeenCalledWith(serverUrl, channelId, callState.recording);
|
||||
expect(setCaptioningState).toHaveBeenCalledWith(serverUrl, channelId, callState.live_captions);
|
||||
});
|
||||
});
|
||||
});
|
||||
275
app/products/calls/hooks.test.ts
Normal file
275
app/products/calls/hooks.test.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, renderHook} from '@testing-library/react-hooks';
|
||||
import {Alert, AppState} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {initializeVoiceTrack} from '@calls/actions/calls';
|
||||
import {
|
||||
getCurrentCall,
|
||||
getCallsConfig,
|
||||
setMicPermissionsGranted,
|
||||
useCallsState,
|
||||
useChannelsWithCalls,
|
||||
useCurrentCall,
|
||||
useGlobalCallsState,
|
||||
useIncomingCalls,
|
||||
} from '@calls/state';
|
||||
import {Screens} from '@constants';
|
||||
import {
|
||||
CURRENT_CALL_BAR_HEIGHT,
|
||||
JOIN_CALL_BAR_HEIGHT,
|
||||
} from '@constants/view';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {queryAllActiveServers} from '@queries/app/servers';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
|
||||
import {useTryCallsFunction, usePermissionsChecker, useCallsAdjustment, useHostControlsAvailable, useHostMenus} from './hooks';
|
||||
|
||||
jest.mock('react-intl', () => ({
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.fn(({defaultMessage}) => defaultMessage),
|
||||
}),
|
||||
defineMessage: (message: any) => message,
|
||||
defineMessages: (messages: any) => messages,
|
||||
}));
|
||||
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn(() => 'server1'),
|
||||
}));
|
||||
|
||||
jest.mock('@context/theme', () => ({
|
||||
useTheme: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
jest.mock('@calls/actions/calls', () => ({
|
||||
initializeVoiceTrack: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@calls/state', () => ({
|
||||
getCurrentCall: jest.fn(),
|
||||
getCallsConfig: jest.fn(),
|
||||
setMicPermissionsGranted: jest.fn(),
|
||||
useCallsState: jest.fn(),
|
||||
useChannelsWithCalls: jest.fn(),
|
||||
useCurrentCall: jest.fn(),
|
||||
useGlobalCallsState: jest.fn(),
|
||||
useIncomingCalls: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@managers/network_manager', () => ({
|
||||
getClient: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/app/servers', () => ({
|
||||
queryAllActiveServers: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/user', () => ({
|
||||
getCurrentUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
openAsBottomSheet: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Calls Hooks', () => {
|
||||
beforeAll(async () => {
|
||||
await DatabaseManager.init(['server1']);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useTryCallsFunction', () => {
|
||||
const mockFn = jest.fn();
|
||||
const mockClient = {
|
||||
getEnabled: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(NetworkManager.getClient as jest.Mock).mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
it('executes function when calls enabled', async () => {
|
||||
mockClient.getEnabled.mockResolvedValue(true);
|
||||
const {result} = renderHook(() => useTryCallsFunction(mockFn));
|
||||
const [tryFn] = result.current;
|
||||
|
||||
await act(async () => {
|
||||
await tryFn();
|
||||
});
|
||||
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows alert when calls not enabled', async () => {
|
||||
mockClient.getEnabled.mockResolvedValue(false);
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
const {result} = renderHook(() => useTryCallsFunction(mockFn));
|
||||
const [tryFn] = result.current;
|
||||
|
||||
await act(async () => {
|
||||
await tryFn();
|
||||
});
|
||||
|
||||
expect(mockFn).not.toHaveBeenCalled();
|
||||
expect(mockAlert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePermissionsChecker', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(AppState, 'addEventListener');
|
||||
});
|
||||
|
||||
it('checks permissions when not granted', async () => {
|
||||
const mockCheck = jest.spyOn(Permissions, 'check').mockResolvedValue(Permissions.RESULTS.GRANTED);
|
||||
const {result} = renderHook(() => usePermissionsChecker(false));
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
// Simulate app becoming active
|
||||
await act(async () => {
|
||||
AppState.currentState = 'active';
|
||||
const callback = (AppState.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
await callback('active');
|
||||
});
|
||||
|
||||
expect(mockCheck).toHaveBeenCalled();
|
||||
expect(initializeVoiceTrack).toHaveBeenCalled();
|
||||
expect(setMicPermissionsGranted).toHaveBeenCalledWith(true);
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('skips check when already granted', () => {
|
||||
const mockCheck = jest.spyOn(Permissions, 'check');
|
||||
renderHook(() => usePermissionsChecker(true));
|
||||
expect(mockCheck).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCallsAdjustment', () => {
|
||||
beforeEach(() => {
|
||||
(useIncomingCalls as jest.Mock).mockReturnValue({incomingCalls: []});
|
||||
(useChannelsWithCalls as jest.Mock).mockReturnValue({});
|
||||
(useCallsState as jest.Mock).mockReturnValue({calls: {}, myUserId: 'user1'});
|
||||
(useGlobalCallsState as jest.Mock).mockReturnValue({micPermissionsGranted: true});
|
||||
(useCurrentCall as jest.Mock).mockReturnValue(null);
|
||||
(queryAllActiveServers as jest.Mock).mockReturnValue({
|
||||
fetch: () => Promise.resolve([{id: 'server1'}]),
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates adjustment with no calls', () => {
|
||||
const {result} = renderHook(() => useCallsAdjustment('server1', 'channel1'));
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
it('includes current call bar height', () => {
|
||||
(useCurrentCall as jest.Mock).mockReturnValue({id: 'call1', channelId: 'channel1'});
|
||||
const {result} = renderHook(() => useCallsAdjustment('server1', 'channel1'));
|
||||
expect(result.current).toBe(CURRENT_CALL_BAR_HEIGHT + 8);
|
||||
});
|
||||
|
||||
it('includes join call banner height', () => {
|
||||
(useChannelsWithCalls as jest.Mock).mockReturnValue({channel1: true});
|
||||
const {result} = renderHook(() => useCallsAdjustment('server1', 'channel1'));
|
||||
expect(result.current).toBe(JOIN_CALL_BAR_HEIGHT + 8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useHostControlsAvailable', () => {
|
||||
beforeEach(() => {
|
||||
(getCallsConfig as jest.Mock).mockReturnValue({
|
||||
HostControlsAllowed: true,
|
||||
});
|
||||
(getCurrentUser as jest.Mock).mockResolvedValue({
|
||||
roles: 'system_user',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true for host', async () => {
|
||||
(getCurrentCall as jest.Mock).mockReturnValueOnce({
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'user1',
|
||||
hostId: 'user1',
|
||||
});
|
||||
|
||||
const {result} = renderHook(() => useHostControlsAvailable());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for admin', async () => {
|
||||
(getCurrentCall as jest.Mock).mockReturnValueOnce({
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'user1',
|
||||
hostId: 'host1',
|
||||
});
|
||||
|
||||
(getCurrentUser as jest.Mock).mockResolvedValueOnce({
|
||||
roles: 'system_admin',
|
||||
});
|
||||
|
||||
const {result, waitForNextUpdate} = renderHook(() => useHostControlsAvailable());
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useHostMenus', () => {
|
||||
const mockSession = {
|
||||
userId: 'user1',
|
||||
sessionId: 'session1',
|
||||
muted: false,
|
||||
raisedHand: 0,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(useCurrentCall as jest.Mock).mockReturnValue({
|
||||
myUserId: 'user1',
|
||||
hostId: 'host1',
|
||||
});
|
||||
(getCallsConfig as jest.Mock).mockReturnValue({
|
||||
HostControlsAllowed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('opens host controls when admin', async () => {
|
||||
(getCurrentUser as jest.Mock).mockResolvedValueOnce({
|
||||
roles: 'system_admin',
|
||||
});
|
||||
|
||||
const {result} = renderHook(() => useHostMenus());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onPress(mockSession)();
|
||||
});
|
||||
|
||||
expect(openAsBottomSheet).toHaveBeenCalledWith(expect.objectContaining({
|
||||
screen: Screens.CALL_HOST_CONTROLS,
|
||||
}));
|
||||
});
|
||||
|
||||
it('opens host controls when host and clicking another profile', async () => {
|
||||
(useCurrentCall as jest.Mock).mockReturnValue({
|
||||
myUserId: 'user1',
|
||||
hostId: 'user1',
|
||||
});
|
||||
|
||||
const {result} = renderHook(() => useHostMenus());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onPress(mockSession)();
|
||||
});
|
||||
|
||||
expect(openAsBottomSheet).toHaveBeenCalledWith(expect.objectContaining({
|
||||
screen: Screens.USER_PROFILE,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
282
app/products/calls/observers/index.test.ts
Normal file
282
app/products/calls/observers/index.test.ts
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {of as of$, firstValueFrom} from 'rxjs';
|
||||
|
||||
import {
|
||||
observeCallsConfig,
|
||||
observeCallsState,
|
||||
observeChannelsWithCalls,
|
||||
observeCurrentCall,
|
||||
observeIncomingCalls,
|
||||
} from '@calls/state';
|
||||
import {DefaultCallsState} from '@calls/types/calls';
|
||||
import {License} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeConfigValue, observeLicense} from '@queries/servers/system';
|
||||
import {queryUsersById} from '@queries/servers/user';
|
||||
|
||||
import {
|
||||
observeIsCallsEnabledInChannel,
|
||||
observeIsCallLimitRestricted,
|
||||
observeCallStateInChannel,
|
||||
observeEndCallDetails,
|
||||
observeCurrentSessionsDict,
|
||||
} from './index';
|
||||
|
||||
jest.mock('@calls/state');
|
||||
jest.mock('@queries/servers/system');
|
||||
jest.mock('@queries/servers/user');
|
||||
|
||||
describe('Calls Observers', () => {
|
||||
const serverUrl = 'https://server.com';
|
||||
const channelId = 'channel1';
|
||||
const database = {} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock DatabaseManager
|
||||
(DatabaseManager as any).serverDatabases = {
|
||||
'test-server': {
|
||||
database: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('observeIsCallsEnabledInChannel', () => {
|
||||
beforeEach(() => {
|
||||
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
||||
pluginEnabled: true,
|
||||
DefaultEnabled: true,
|
||||
}));
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$(DefaultCallsState));
|
||||
(observeConfigValue as jest.Mock).mockReturnValue(of$('7.6.0'));
|
||||
});
|
||||
|
||||
it('should return false when plugin is disabled regardless of other settings', async () => {
|
||||
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
||||
pluginEnabled: false,
|
||||
DefaultEnabled: true,
|
||||
}));
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$(DefaultCallsState));
|
||||
(observeConfigValue as jest.Mock).mockReturnValue(of$('10.5.0'));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when explicitly enabled', async () => {
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
||||
enabled: {[channelId]: true},
|
||||
}));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when explicitly disabled', async () => {
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
||||
enabled: {[channelId]: false},
|
||||
}));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for GA server when not explicitly disabled', async () => {
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({enabled: {}}));
|
||||
(observeConfigValue as jest.Mock).mockReturnValue(of$('7.6.0'));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should use default enabled when not GA server and not explicitly set', async () => {
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({enabled: {}}));
|
||||
(observeConfigValue as jest.Mock).mockReturnValue(of$('7.5.0'));
|
||||
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
||||
pluginEnabled: true,
|
||||
DefaultEnabled: true,
|
||||
}));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallsEnabledInChannel(database, serverUrl, of$(channelId)));
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeIsCallLimitRestricted', () => {
|
||||
it('should detect when call limit is not restricted', async () => {
|
||||
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
||||
MaxCallParticipants: 8,
|
||||
sku_short_name: License.SKU_SHORT_NAME.Professional,
|
||||
}));
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
||||
calls: {
|
||||
[channelId]: {
|
||||
sessions: {user1: {}, user2: {}},
|
||||
},
|
||||
},
|
||||
}));
|
||||
(observeLicense as jest.Mock).mockReturnValue(of$({Cloud: 'false'}));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallLimitRestricted(database, serverUrl, channelId));
|
||||
expect(result).toEqual({
|
||||
limitRestricted: false,
|
||||
maxParticipants: 8,
|
||||
isCloudStarter: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect when call limit is restricted', async () => {
|
||||
(observeCallsConfig as jest.Mock).mockReturnValue(of$({
|
||||
MaxCallParticipants: 3,
|
||||
sku_short_name: License.SKU_SHORT_NAME.Starter,
|
||||
}));
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
||||
calls: {
|
||||
[channelId]: {
|
||||
sessions: {user1: {}, user2: {}, user3: {}},
|
||||
},
|
||||
},
|
||||
}));
|
||||
(observeLicense as jest.Mock).mockReturnValue(of$({Cloud: 'true'}));
|
||||
|
||||
const result = await firstValueFrom(observeIsCallLimitRestricted(database, serverUrl, channelId));
|
||||
expect(result).toEqual({
|
||||
limitRestricted: true,
|
||||
maxParticipants: 3,
|
||||
isCloudStarter: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeCallStateInChannel', () => {
|
||||
it('should not show banner when no call in channel', async () => {
|
||||
(observeChannelsWithCalls as jest.Mock).mockReturnValue(of$({}));
|
||||
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
||||
calls: {},
|
||||
myUserId: 'user1',
|
||||
}));
|
||||
(observeIncomingCalls as jest.Mock).mockReturnValue(of$({
|
||||
incomingCalls: [],
|
||||
}));
|
||||
|
||||
const {showJoinCallBanner, isInACall, showIncomingCalls} = observeCallStateInChannel(serverUrl, database, of$(channelId));
|
||||
|
||||
const bannerVisible = await firstValueFrom(showJoinCallBanner);
|
||||
const inCall = await firstValueFrom(isInACall);
|
||||
const hasIncoming = await firstValueFrom(showIncomingCalls);
|
||||
|
||||
expect(bannerVisible).toBe(false);
|
||||
expect(inCall).toBe(false);
|
||||
expect(hasIncoming).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect active call in channel', async () => {
|
||||
(observeChannelsWithCalls as jest.Mock).mockReturnValue(of$({
|
||||
[channelId]: true,
|
||||
}));
|
||||
(observeCurrentCall as jest.Mock).mockReturnValue(of$({
|
||||
channelId: 'different-channel',
|
||||
connected: true,
|
||||
}));
|
||||
(observeCallsState as jest.Mock).mockReturnValue(of$({
|
||||
calls: {
|
||||
[channelId]: {
|
||||
dismissed: {},
|
||||
},
|
||||
},
|
||||
myUserId: 'user1',
|
||||
}));
|
||||
(observeIncomingCalls as jest.Mock).mockReturnValue(of$({
|
||||
incomingCalls: [],
|
||||
}));
|
||||
|
||||
const {showJoinCallBanner, isInACall, showIncomingCalls} = observeCallStateInChannel(serverUrl, database, of$(channelId));
|
||||
|
||||
const bannerVisible = await firstValueFrom(showJoinCallBanner);
|
||||
const inCall = await firstValueFrom(isInACall);
|
||||
const hasIncoming = await firstValueFrom(showIncomingCalls);
|
||||
|
||||
expect(bannerVisible).toBe(true);
|
||||
expect(inCall).toBe(true);
|
||||
expect(hasIncoming).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeCurrentSessionsDict', () => {
|
||||
it('should handle empty/null call state', async () => {
|
||||
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
||||
|
||||
const result = await firstValueFrom(observeCurrentSessionsDict());
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should fill user models for sessions', async () => {
|
||||
const sessions = {
|
||||
session1: {userId: 'user1'},
|
||||
session2: {userId: 'user2'},
|
||||
};
|
||||
(observeCurrentCall as jest.Mock).mockReturnValue(of$({
|
||||
sessions,
|
||||
serverUrl: 'test-server',
|
||||
}));
|
||||
const userModels = [
|
||||
{id: 'user1', username: 'user.one'},
|
||||
{id: 'user2', username: 'user.two'},
|
||||
];
|
||||
(queryUsersById as jest.Mock).mockReturnValue({
|
||||
observeWithColumns: () => of$(userModels),
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(observeCurrentSessionsDict());
|
||||
expect(result).toEqual({
|
||||
session1: {...sessions.session1, userModel: userModels[0]},
|
||||
session2: {...sessions.session2, userModel: userModels[1]},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeEndCallDetails', () => {
|
||||
it('should handle empty call state', async () => {
|
||||
(observeCurrentCall as jest.Mock).mockReturnValue(of$(null));
|
||||
|
||||
const {otherParticipants, isAdmin, isHost} = observeEndCallDetails();
|
||||
|
||||
const hasOthers = await firstValueFrom(otherParticipants);
|
||||
const admin = await firstValueFrom(isAdmin);
|
||||
const host = await firstValueFrom(isHost);
|
||||
|
||||
expect(hasOthers).toBe(false);
|
||||
expect(admin).toBe(false);
|
||||
expect(host).toBe(false);
|
||||
});
|
||||
|
||||
it('should determine host and admin status', async () => {
|
||||
(observeCurrentCall as jest.Mock).mockReturnValue(of$({
|
||||
myUserId: 'user1',
|
||||
mySessionId: 'session1',
|
||||
hostId: 'user1',
|
||||
sessions: {
|
||||
session1: {
|
||||
userModel: {
|
||||
roles: 'system_admin',
|
||||
},
|
||||
},
|
||||
session2: {},
|
||||
},
|
||||
}));
|
||||
|
||||
const {otherParticipants, isAdmin, isHost} = observeEndCallDetails();
|
||||
|
||||
const hasOthers = await firstValueFrom(otherParticipants);
|
||||
const admin = await firstValueFrom(isAdmin);
|
||||
const host = await firstValueFrom(isHost);
|
||||
|
||||
expect(hasOthers).toBe(true);
|
||||
expect(admin).toBe(true);
|
||||
expect(host).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -158,7 +158,7 @@ export const observeEndCallDetails = () => {
|
|||
distinctUntilChanged(),
|
||||
);
|
||||
const isHost = cc.pipe(
|
||||
switchMap((call) => of$(call?.hostId === call?.myUserId)),
|
||||
switchMap((call) => of$(call ? call.hostId === call.myUserId : false)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import {act, renderHook} from '@testing-library/react-hooks'; // Use instead of react-native version due to different behavior. Consider migrating
|
||||
import {AppState} from 'react-native';
|
||||
|
||||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setHost,
|
||||
setIncomingCalls,
|
||||
setJoiningChannelId,
|
||||
setMicPermissionsErrorDismissed,
|
||||
setMicPermissionsGranted,
|
||||
|
|
@ -30,6 +32,7 @@ import {
|
|||
useGlobalCallsState,
|
||||
useIncomingCalls,
|
||||
userReacted,
|
||||
setCallForChannel,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
callEnded,
|
||||
|
|
@ -48,6 +51,8 @@ import {
|
|||
setUserVoiceOn,
|
||||
userJoinedCall,
|
||||
userLeftCall,
|
||||
callsOnAppStateChange,
|
||||
playIncomingCallsRinging,
|
||||
} from '@calls/state/actions';
|
||||
import {
|
||||
AudioDevice,
|
||||
|
|
@ -95,6 +100,7 @@ jest.mock('@queries/servers/user', () => ({
|
|||
getUserById: jest.fn(() => Promise.resolve({
|
||||
username: 'user-5',
|
||||
})),
|
||||
getCurrentUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-native-navigation', () => ({
|
||||
|
|
@ -1440,6 +1446,177 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[2], expectedIncomingCalls);
|
||||
});
|
||||
|
||||
it('playIncomingCallsRinging', async () => {
|
||||
const initialIncomingCalls = {
|
||||
...DefaultIncomingCalls,
|
||||
incomingCalls: [{
|
||||
callID: 'callDM',
|
||||
callerID: 'user-5',
|
||||
callerModel: {username: 'user-5'} as UserModel,
|
||||
channelID: 'channel-private',
|
||||
myUserId: 'myId',
|
||||
serverUrl: 'server1',
|
||||
startAt: 123,
|
||||
type: 0,
|
||||
}],
|
||||
currentRingingCallId: undefined,
|
||||
};
|
||||
|
||||
// setup
|
||||
await DatabaseManager.init(['server1']);
|
||||
const {result} = renderHook(() => useIncomingCalls());
|
||||
await act(async () => {
|
||||
await setIncomingCalls(initialIncomingCalls);
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
AppState.currentState = 'active';
|
||||
|
||||
const getCurrentUser = require('@queries/servers/user').getCurrentUser;
|
||||
getCurrentUser.mockResolvedValueOnce({
|
||||
id: 'user-5',
|
||||
roles: 'user',
|
||||
notifyProps: {
|
||||
calls_mobile_sound: 'true',
|
||||
calls_mobile_notification_sound: 'Calm',
|
||||
},
|
||||
});
|
||||
|
||||
// test: should not ring when in DND
|
||||
await act(async () => {
|
||||
await playIncomingCallsRinging('server1', 'call1', 'dnd');
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
|
||||
// test: should not ring when in OOO
|
||||
await act(async () => {
|
||||
await playIncomingCallsRinging('server1', 'call1', 'ooo');
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
|
||||
// test: should ring when online
|
||||
await act(async () => {
|
||||
await playIncomingCallsRinging('server1', 'call1', 'online');
|
||||
});
|
||||
assert.deepEqual(result.current, {
|
||||
...initialIncomingCalls,
|
||||
currentRingingCallId: 'call1',
|
||||
callIdHasRung: {call1: true},
|
||||
});
|
||||
|
||||
// test: should not ring for same call again
|
||||
await act(async () => {
|
||||
await setIncomingCalls(initialIncomingCalls);
|
||||
await playIncomingCallsRinging('server1', 'call1', 'online');
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
|
||||
// test: should not ring when already ringing
|
||||
await act(async () => {
|
||||
await setIncomingCalls({
|
||||
...initialIncomingCalls,
|
||||
currentRingingCallId: 'call2',
|
||||
});
|
||||
await playIncomingCallsRinging('server1', 'call3', 'online');
|
||||
});
|
||||
assert.deepEqual(result.current.currentRingingCallId, 'call2');
|
||||
});
|
||||
|
||||
it('callsOnAppStateChange', async () => {
|
||||
const initialIncomingCalls = {
|
||||
...DefaultIncomingCalls,
|
||||
currentRingingCallId: 'call1',
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => useIncomingCalls());
|
||||
await act(async () => {
|
||||
await setIncomingCalls(initialIncomingCalls);
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
|
||||
// test going to background
|
||||
await act(async () => callsOnAppStateChange('background'));
|
||||
assert.deepEqual(result.current, {...initialIncomingCalls, currentRingingCallId: undefined});
|
||||
|
||||
// test going to inactive
|
||||
await act(async () => {
|
||||
await setIncomingCalls(initialIncomingCalls);
|
||||
await callsOnAppStateChange('inactive');
|
||||
});
|
||||
assert.deepEqual(result.current, {...initialIncomingCalls, currentRingingCallId: undefined});
|
||||
|
||||
// test going to active (should not change state)
|
||||
await act(async () => {
|
||||
await setIncomingCalls(initialIncomingCalls);
|
||||
await callsOnAppStateChange('active');
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
|
||||
// test previous state
|
||||
await act(async () => {
|
||||
await setIncomingCalls(initialIncomingCalls);
|
||||
await callsOnAppStateChange('active');
|
||||
});
|
||||
assert.deepEqual(result.current, initialIncomingCalls);
|
||||
});
|
||||
|
||||
it('setCallForChannel', async () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
calls: {'channel-1': call1},
|
||||
enabled: {'channel-1': true},
|
||||
};
|
||||
const initialChannelsWithCalls = {'channel-1': true};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()];
|
||||
});
|
||||
await act(async () => {
|
||||
await setCallsState('server1', initialCallsState);
|
||||
await setChannelsWithCalls('server1', initialChannelsWithCalls);
|
||||
await setCurrentCall(initialCurrentCallState);
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], initialChannelsWithCalls);
|
||||
assert.deepEqual(result.current[2], initialCurrentCallState);
|
||||
|
||||
// test: add new call
|
||||
await act(async () => setCallForChannel('server1', 'channel-2', call2, true));
|
||||
assert.deepEqual(result.current[0], {
|
||||
...initialCallsState,
|
||||
calls: {...initialCallsState.calls, 'channel-2': call2},
|
||||
enabled: {...initialCallsState.enabled, 'channel-2': true},
|
||||
});
|
||||
assert.deepEqual(result.current[1], {...initialChannelsWithCalls, 'channel-2': true});
|
||||
|
||||
// test: update existing call
|
||||
const updatedCall1 = {...call1, screenOn: 'newScreen'};
|
||||
await act(async () => setCallForChannel('server1', 'channel-1', updatedCall1));
|
||||
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'], updatedCall1);
|
||||
assert.deepEqual(result.current[2], {...initialCurrentCallState, screenOn: 'newScreen'});
|
||||
|
||||
// test: remove call
|
||||
await act(async () => setCallForChannel('server1', 'channel-1', undefined));
|
||||
assert.deepEqual(result.current[0], {
|
||||
...initialCallsState,
|
||||
calls: {'channel-2': call2},
|
||||
enabled: {'channel-1': true, 'channel-2': true},
|
||||
});
|
||||
assert.deepEqual(result.current[1], {'channel-2': true});
|
||||
|
||||
// test: just update enabled state
|
||||
await act(async () => setCallForChannel('server1', 'channel-3', undefined, true));
|
||||
assert.deepEqual((result.current[0] as CallsState).enabled['channel-3'], true);
|
||||
assert.deepEqual(result.current[1], {'channel-2': true});
|
||||
});
|
||||
|
||||
it('captions', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,33 @@
|
|||
|
||||
import assert from 'assert';
|
||||
|
||||
import {type CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
|
||||
import {License} from '@constants';
|
||||
import {Alert} from 'react-native';
|
||||
import {SelectedTrackType} from 'react-native-video';
|
||||
|
||||
import {getICEServersConfigs, getCallPropsFromPost} from './utils';
|
||||
import {type CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
|
||||
import {License, Post} from '@constants';
|
||||
import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification';
|
||||
|
||||
import {
|
||||
getICEServersConfigs,
|
||||
getCallPropsFromPost,
|
||||
sortSessions,
|
||||
getHandsRaised,
|
||||
getHandsRaisedNames,
|
||||
isSupportedServerCalls,
|
||||
isHostControlsAllowed,
|
||||
areGroupCallsAllowed,
|
||||
isCallsCustomMessage,
|
||||
idsAreEqual,
|
||||
errorAlert,
|
||||
makeCallsTheme,
|
||||
isCallsStartedMessage,
|
||||
hasCaptions,
|
||||
getTranscriptionUri,
|
||||
} from './utils';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
describe('getICEServersConfigs', () => {
|
||||
it('backwards compatible case, no ICEServersConfigs present', () => {
|
||||
|
|
@ -98,6 +121,298 @@ describe('getICEServersConfigs', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('sortSessions', () => {
|
||||
const locale = 'en';
|
||||
const teammateNameDisplay = 'username';
|
||||
|
||||
it('returns empty array for undefined sessions', () => {
|
||||
expect(sortSessions(locale, teammateNameDisplay, undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('sorts by name', () => {
|
||||
const sessions = {
|
||||
1: {
|
||||
sessionId: '1',
|
||||
userId: 'user1',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
userModel: {username: 'charlie'} as UserModel,
|
||||
},
|
||||
2: {
|
||||
sessionId: '2',
|
||||
userId: 'user2',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
userModel: {username: 'alice'} as UserModel,
|
||||
},
|
||||
3: {
|
||||
sessionId: '3',
|
||||
userId: 'user3',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
userModel: {username: 'bob'} as UserModel,
|
||||
},
|
||||
};
|
||||
|
||||
const sorted = sortSessions(locale, teammateNameDisplay, sessions);
|
||||
expect(sorted.map((s) => s.userModel?.username)).toEqual(['alice', 'bob', 'charlie']);
|
||||
});
|
||||
|
||||
it('sorts by state (presenter > raised hand > unmuted)', () => {
|
||||
const sessions = {
|
||||
1: {
|
||||
sessionId: '1',
|
||||
userId: 'user1',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
userModel: {username: 'a'} as UserModel,
|
||||
},
|
||||
2: {
|
||||
sessionId: '2',
|
||||
userId: 'user2',
|
||||
muted: false,
|
||||
raisedHand: 0,
|
||||
userModel: {username: 'b'} as UserModel,
|
||||
},
|
||||
3: {
|
||||
sessionId: '3',
|
||||
userId: 'user3',
|
||||
muted: true,
|
||||
raisedHand: 1000,
|
||||
userModel: {username: 'c'} as UserModel,
|
||||
},
|
||||
};
|
||||
|
||||
const sorted = sortSessions(locale, teammateNameDisplay, sessions, '2');
|
||||
expect(sorted.map((s) => s.userModel?.username)).toEqual(['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
it('sorts by raised hand timestamp when multiple hands are raised', () => {
|
||||
const sessions = {
|
||||
1: {
|
||||
sessionId: '1',
|
||||
userId: 'user1',
|
||||
muted: true,
|
||||
raisedHand: 2000, // Raised hand second
|
||||
userModel: {username: 'a'} as UserModel,
|
||||
},
|
||||
2: {
|
||||
sessionId: '2',
|
||||
userId: 'user2',
|
||||
muted: true,
|
||||
raisedHand: 1000, // Raised hand first
|
||||
userModel: {username: 'b'} as UserModel,
|
||||
},
|
||||
};
|
||||
|
||||
const sorted = sortSessions(locale, teammateNameDisplay, sessions);
|
||||
expect(sorted.map((s) => s.userModel?.username)).toEqual(['b', 'a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHandsRaised', () => {
|
||||
it('returns sessions with raised hands', () => {
|
||||
const sessions = {
|
||||
1: {
|
||||
sessionId: '1',
|
||||
userId: 'user1',
|
||||
muted: true,
|
||||
raisedHand: 0,
|
||||
},
|
||||
2: {
|
||||
sessionId: '2',
|
||||
userId: 'user2',
|
||||
muted: true,
|
||||
raisedHand: 1000,
|
||||
},
|
||||
3: {
|
||||
sessionId: '3',
|
||||
userId: 'user3',
|
||||
muted: true,
|
||||
raisedHand: 2000,
|
||||
},
|
||||
};
|
||||
|
||||
const raised = getHandsRaised(sessions);
|
||||
expect(raised.length).toBe(2);
|
||||
expect(raised.map((s) => s.sessionId).sort()).toEqual(['2', '3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHandsRaisedNames', () => {
|
||||
const locale = 'en';
|
||||
const teammateNameDisplay = 'username';
|
||||
const intl = {
|
||||
formatMessage: ({id}: {id: string}) => (id === 'mobile.calls_you_2' ? 'You' : id),
|
||||
} as IntlShape;
|
||||
|
||||
it('returns names in order of raised hand time', () => {
|
||||
const sessions = [
|
||||
{
|
||||
sessionId: '1',
|
||||
userId: 'user1',
|
||||
raisedHand: 2000,
|
||||
userModel: {username: 'alice'} as UserModel,
|
||||
muted: false,
|
||||
},
|
||||
{
|
||||
sessionId: '2',
|
||||
userId: 'user2',
|
||||
raisedHand: 1000,
|
||||
userModel: {username: 'bob'} as UserModel,
|
||||
muted: false,
|
||||
},
|
||||
];
|
||||
|
||||
const names = getHandsRaisedNames(sessions, '3', locale, teammateNameDisplay, intl);
|
||||
expect(names).toEqual(['bob', 'alice']);
|
||||
});
|
||||
|
||||
it('shows "You" for current user', () => {
|
||||
const sessions = [
|
||||
{
|
||||
sessionId: '1',
|
||||
userId: 'user1',
|
||||
raisedHand: 1000,
|
||||
userModel: {username: 'alice'} as UserModel,
|
||||
muted: false,
|
||||
},
|
||||
];
|
||||
|
||||
const names = getHandsRaisedNames(sessions, '1', locale, teammateNameDisplay, intl);
|
||||
expect(names).toEqual(['You']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedServerCalls', () => {
|
||||
it('returns false for undefined version', () => {
|
||||
expect(isSupportedServerCalls(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for supported version', () => {
|
||||
expect(isSupportedServerCalls('7.6.0')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for unsupported version', () => {
|
||||
expect(isSupportedServerCalls('6.2.0')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHostControlsAllowed and areGroupCallsAllowed', () => {
|
||||
const config: CallsConfigState = {
|
||||
...DefaultCallsConfig,
|
||||
HostControlsAllowed: true,
|
||||
GroupCallsAllowed: true,
|
||||
};
|
||||
|
||||
it('returns config values', () => {
|
||||
expect(isHostControlsAllowed(config)).toBe(true);
|
||||
expect(areGroupCallsAllowed(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for undefined values', () => {
|
||||
expect(isHostControlsAllowed({} as CallsConfigState)).toBe(false);
|
||||
expect(areGroupCallsAllowed({} as CallsConfigState)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCallsCustomMessage', () => {
|
||||
it('identifies calls messages', () => {
|
||||
expect(isCallsCustomMessage({type: Post.POST_TYPES.CUSTOM_CALLS} as Post)).toBe(true);
|
||||
expect(isCallsCustomMessage({type: 'regular_post' as unknown} as Post)).toBe(false);
|
||||
expect(isCallsCustomMessage({} as Post)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('idsAreEqual', () => {
|
||||
it('compares arrays of ids', () => {
|
||||
expect(idsAreEqual(['1', '2', '3'], ['1', '2', '3'])).toBe(true);
|
||||
expect(idsAreEqual(['1', '2', '3'], ['3', '2', '1'])).toBe(true);
|
||||
expect(idsAreEqual(['1', '2'], ['1', '2', '3'])).toBe(false);
|
||||
expect(idsAreEqual(['1', '2', '3'], ['1', '2'])).toBe(false);
|
||||
expect(idsAreEqual(['1', '2', '3'], ['4', '5', '6'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errorAlert', () => {
|
||||
it('shows error alert', () => {
|
||||
const mockAlert = jest.spyOn(Alert, 'alert');
|
||||
const intl = {
|
||||
formatMessage: ({defaultMessage}: {defaultMessage: string}, values?: any) => {
|
||||
if (values) {
|
||||
return defaultMessage.replace('{error}', values.error);
|
||||
}
|
||||
return defaultMessage;
|
||||
},
|
||||
} as IntlShape;
|
||||
|
||||
errorAlert('test error', intl);
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith(
|
||||
'Error',
|
||||
'Error: test error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeCallsTheme', () => {
|
||||
it('creates calls theme from base theme', () => {
|
||||
const theme = {
|
||||
sidebarBg: '#000000',
|
||||
} as Theme;
|
||||
|
||||
const callsTheme = makeCallsTheme(theme);
|
||||
|
||||
expect(callsTheme.callsBg).toBeDefined();
|
||||
expect(callsTheme.callsBgRgb).toBeDefined();
|
||||
expect(callsTheme.callsBadgeBg).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCallsStartedMessage', () => {
|
||||
it('identifies calls notifications', () => {
|
||||
expect(isCallsStartedMessage({sub_type: NOTIFICATION_SUB_TYPE.CALLS} as NotificationData)).toBe(true);
|
||||
expect(isCallsStartedMessage({message: "You've been invited to a call"} as NotificationData)).toBe(true);
|
||||
expect(isCallsStartedMessage({message: '\u200bUser is inviting you to a call'} as NotificationData)).toBe(true);
|
||||
expect(isCallsStartedMessage({message: 'regular message'} as NotificationData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCaptions', () => {
|
||||
it('checks for valid captions', () => {
|
||||
expect(hasCaptions({captions: [{title: 'test', language: 'en', file_id: '123'}]})).toBe(true);
|
||||
expect(hasCaptions({captions: []})).toBe(false);
|
||||
expect(hasCaptions({})).toBe(false);
|
||||
expect(hasCaptions(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTranscriptionUri', () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
|
||||
it('returns empty track when no captions', () => {
|
||||
const result = getTranscriptionUri(serverUrl, {});
|
||||
expect(result.tracks).toBeUndefined();
|
||||
expect(result.selected).toEqual({type: SelectedTrackType.DISABLED, value: ''});
|
||||
});
|
||||
|
||||
it('returns track info when captions exist', () => {
|
||||
const props = {
|
||||
captions: [{
|
||||
title: 'English',
|
||||
language: 'en',
|
||||
file_id: '123',
|
||||
}],
|
||||
};
|
||||
|
||||
const result = getTranscriptionUri(serverUrl, props);
|
||||
|
||||
expect(result.tracks).toBeDefined();
|
||||
expect(result.tracks?.length).toBe(1);
|
||||
expect(result.selected).toEqual({type: SelectedTrackType.INDEX, value: 0});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCallPropsFromPost', () => {
|
||||
test('undefined props', () => {
|
||||
const post = {} as Post;
|
||||
|
|
|
|||
Loading…
Reference in a new issue