Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,8 @@ e2e/e2e_account.ts
**/e2e_account.js
**/e2e_account.ts

*.p8
*.p8

# Local tooling
.claude/
.superset/
25 changes: 9 additions & 16 deletions app/containers/NewMediaCall/CreateCall.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,17 @@ import { mockedStore } from '../../reducers/mockedStore';
import type { TPeerItem } from '../../lib/services/voip/getPeerAutocompleteOptions';
import * as stories from './CreateCall.stories';
import { generateSnapshots } from '../../../.rnstorybook/generateSnapshots';
import { mediaSessionInstance } from '../../lib/services/voip/MediaSessionInstance';

const mockStartCall = jest.fn();
const mockHideActionSheet = jest.fn();

jest.mock('../../lib/services/voip/MediaSessionInstance', () => {
const instance = {};
Object.defineProperty(instance, 'startCall', {
value: (...args: unknown[]) => mockStartCall(...args),
writable: false,
configurable: false
});
return { mediaSessionInstance: instance };
});

jest.mock('../ActionSheet', () => ({
...jest.requireActual('../ActionSheet'),
hideActionSheetRef: () => mockHideActionSheet()
}));

jest.spyOn(mediaSessionInstance, 'startCall').mockReturnValue({ success: true, callId: '' });

const setStoreState = (selectedPeer: TPeerItem | null) => {
usePeerAutocompleteStore.setState({ selectedPeer });
};
Expand All @@ -50,6 +42,7 @@ describe('CreateCall', () => {
beforeEach(() => {
jest.clearAllMocks();
usePeerAutocompleteStore.setState({ selectedPeer: null });
jest.spyOn(mediaSessionInstance, 'startCall').mockReturnValue({ success: true, callId: '' });
});

it('should render the call button', () => {
Expand Down Expand Up @@ -83,7 +76,7 @@ describe('CreateCall', () => {
);

fireEvent.press(getByTestId('new-media-call-button'));
expect(mockStartCall).not.toHaveBeenCalled();
expect(mediaSessionInstance.startCall).not.toHaveBeenCalled();
expect(mockHideActionSheet).not.toHaveBeenCalled();
});

Expand All @@ -108,8 +101,8 @@ describe('CreateCall', () => {
);

fireEvent.press(getByTestId('new-media-call-button'));
expect(mockStartCall).toHaveBeenCalledTimes(1);
expect(mockStartCall).toHaveBeenCalledWith('user-1', 'user');
expect(mediaSessionInstance.startCall).toHaveBeenCalledTimes(1);
expect(mediaSessionInstance.startCall).toHaveBeenCalledWith('user-1', 'user');
expect(mockHideActionSheet).toHaveBeenCalledTimes(1);
});

Expand All @@ -122,8 +115,8 @@ describe('CreateCall', () => {
);

fireEvent.press(getByTestId('new-media-call-button'));
expect(mockStartCall).toHaveBeenCalledTimes(1);
expect(mockStartCall).toHaveBeenCalledWith('+5511999999999', 'sip');
expect(mediaSessionInstance.startCall).toHaveBeenCalledTimes(1);
expect(mediaSessionInstance.startCall).toHaveBeenCalledWith('+5511999999999', 'sip');
expect(mockHideActionSheet).toHaveBeenCalledTimes(1);
});
});
Expand Down
1 change: 1 addition & 0 deletions app/lib/services/voip/MediaCallEvents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jest.mock('react-native-callkeep', () => ({

jest.mock('./MediaSessionInstance', () => ({
mediaSessionInstance: {
startCall: (...args: unknown[]) => ({ success: true, callId: '' }),
endCall: jest.fn()
}
Comment on lines 52 to 56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a mock function for startCall.

The rest parameter here is unused, and ESLint is already flagging it. Switching this to jest.fn(() => ({ success: true, callId: '' })) removes the lint failure and keeps the mock inspectable.

🧰 Tools
🪛 ESLint

[error] 54-54: 'args' is defined but never used. Allowed unused args must match /^_/u.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/voip/MediaCallEvents.test.ts` around lines 52 - 56, The test
mock uses a plain function with an unused rest parameter for
mediaSessionInstance.startCall which trips ESLint; replace that implementation
with a Jest mock so it is inspectable and lint-clean, i.e., change the startCall
value in the jest.mock for MediaSessionInstance to jest.fn(() => ({ success:
true, callId: '' })) and keep endCall as jest.fn() so tests can assert calls on
mediaSessionInstance.startCall.

}));
Expand Down
127 changes: 127 additions & 0 deletions app/lib/services/voip/MediaSessionController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { IClientMediaCall } from '@rocket.chat/media-signaling';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the unused type import.

IClientMediaCall is not referenced anywhere in this file, so this currently fails @typescript-eslint/no-unused-vars.

🧰 Tools
🪛 ESLint

[error] 1-1: 'IClientMediaCall' is defined but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/voip/MediaSessionController.test.ts` at line 1, The test
file imports an unused type IClientMediaCall which triggers
`@typescript-eslint/no-unused-vars`; remove the import declaration referencing
IClientMediaCall from MediaSessionController.test.ts (the import line: import
type { IClientMediaCall } from '@rocket.chat/media-signaling';) or, if the type
is intended to be used, replace its usage accordingly—otherwise simply delete
that named type from the import so only used symbols remain.


import { mediaSessionStore } from './MediaSessionStore';
import { MediaSessionController } from './MediaSessionController';

jest.mock('./MediaSessionStore', () => ({
mediaSessionStore: {
setWebRTCProcessorFactory: jest.fn(),
getInstance: jest.fn(),
dispose: jest.fn(),
onChange: jest.fn(() => jest.fn())
}
}));

jest.mock('../sdk', () => ({
default: {
onStreamData: jest.fn(() => ({ stop: jest.fn() }))
}
}));

jest.mock('react-native-device-info', () => ({
getUniqueIdSync: () => 'device-123'
}));

jest.mock('../../store/auxStore', () => ({
store: {
getState: () => ({
settings: {
VoIP_TeamCollab_Ice_Servers: 'stun:stun.l.google.com:19302',
VoIP_TeamCollab_Ice_Gathering_Timeout: 5000
}
}),
subscribe: jest.fn(() => jest.fn())
}
}));

type MockMediaSignalingSession = {
userId: string;
on: jest.Mock;
setIceGatheringTimeout: jest.Mock;
};

jest.mock('@rocket.chat/media-signaling', () => ({
MediaCallWebRTCProcessor: jest.fn().mockImplementation(function (this: unknown) {
return this;
}),
MediaSignalingSession: jest
.fn()
.mockImplementation(function MockMediaSignalingSession(this: MockMediaSignalingSession, config: { userId: string }) {
this.userId = config.userId;
this.on = jest.fn();
this.setIceGatheringTimeout = jest.fn();
})
}));

jest.mock('react-native-webrtc', () => ({
registerGlobals: jest.fn()
}));

describe('MediaSessionController', () => {
afterEach(() => {
jest.clearAllMocks();
});

describe('constructor', () => {
it('should initialize with empty session', () => {
const controller = new MediaSessionController('user-123');
expect(controller.getSession()).toBeNull();
});
});

describe('configure', () => {
it('should set WebRTC processor factory with ICE servers', () => {
const controller = new MediaSessionController('user-123');
controller.configure();

expect(mediaSessionStore.setWebRTCProcessorFactory).toHaveBeenCalledTimes(1);
const factory = (mediaSessionStore.setWebRTCProcessorFactory as jest.Mock).mock.calls[0][0];
const processor = factory({
rtc: { iceServers: [] },
iceGatheringTimeout: 5000
});
expect(processor).toBeDefined();
});

it('should create session via mediaSessionStore', () => {
const mockInstance = { startCall: jest.fn() };
(mediaSessionStore.getInstance as jest.Mock).mockReturnValue(mockInstance);

const controller = new MediaSessionController('user-123');
controller.configure();

expect(mediaSessionStore.getInstance).toHaveBeenCalledWith('user-123');
});
});

describe('getSession', () => {
it('should return null before configure', () => {
const controller = new MediaSessionController('user-123');
expect(controller.getSession()).toBeNull();
});

it('should return session after configure', () => {
const mockInstance = { startCall: jest.fn() };
(mediaSessionStore.getInstance as jest.Mock).mockReturnValue(mockInstance);

const controller = new MediaSessionController('user-123');
controller.configure();

expect(controller.getSession()).toBe(mockInstance);
});
});

describe('reset', () => {
it('should dispose mediaSessionStore and set session to null', () => {
const mockInstance = { startCall: jest.fn() };
(mediaSessionStore.getInstance as jest.Mock).mockReturnValue(mockInstance);

const controller = new MediaSessionController('user-123');
controller.configure();
controller.reset();

expect(mediaSessionStore.dispose).toHaveBeenCalled();
expect(controller.getSession()).toBeNull();
});
});
});
87 changes: 87 additions & 0 deletions app/lib/services/voip/MediaSessionController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
MediaCallWebRTCProcessor,
type WebRTCProcessorConfig,
type MediaSignalingSession
} from '@rocket.chat/media-signaling';
import { registerGlobals } from 'react-native-webrtc';

import { mediaSessionStore } from './MediaSessionStore';
import { parseStringToIceServers } from './parseStringToIceServers';
import { store } from '../../store/auxStore';
import type { IceServer } from '../../../definitions/Voip';

export class MediaSessionController {
private userId: string;
private session: MediaSignalingSession | null = null;
private iceServers: IceServer[] = [];
private iceGatheringTimeout: number = 5000;
private storeTimeoutUnsubscribe: (() => void) | null = null;
private storeIceServersUnsubscribe: (() => void) | null = null;

constructor(userId: string) {
this.userId = userId;
}

public configure(): void {
registerGlobals();
this.configureIceServers();

mediaSessionStore.setWebRTCProcessorFactory(
(config: WebRTCProcessorConfig) =>
new MediaCallWebRTCProcessor({
...config,
rtc: { ...config.rtc, iceServers: this.iceServers },
iceGatheringTimeout: this.iceGatheringTimeout
})
);

this.session = mediaSessionStore.getInstance(this.userId);
}

public getSession(): MediaSignalingSession | null {
return this.session;
}

public refreshSession(): MediaSignalingSession | null {
this.session = mediaSessionStore.getInstance(this.userId);
return this.session;
}

public reset(): void {
if (this.storeTimeoutUnsubscribe) {
this.storeTimeoutUnsubscribe();
this.storeTimeoutUnsubscribe = null;
}
if (this.storeIceServersUnsubscribe) {
this.storeIceServersUnsubscribe();
this.storeIceServersUnsubscribe = null;
}
mediaSessionStore.dispose();
this.session = null;
}

private getIceServers(): IceServer[] {
const iceServers = store.getState().settings.VoIP_TeamCollab_Ice_Servers as string;
return parseStringToIceServers(iceServers);
}

private configureIceServers(): void {
this.iceServers = this.getIceServers();
this.iceGatheringTimeout = store.getState().settings.VoIP_TeamCollab_Ice_Gathering_Timeout as number;

this.storeTimeoutUnsubscribe = store.subscribe(() => {
const currentTimeout = store.getState().settings.VoIP_TeamCollab_Ice_Gathering_Timeout as number;
if (currentTimeout !== this.iceGatheringTimeout) {
this.iceGatheringTimeout = currentTimeout;
this.session?.setIceGatheringTimeout(this.iceGatheringTimeout);
}
});

this.storeIceServersUnsubscribe = store.subscribe(() => {
const currentIceServers = this.getIceServers();
if (currentIceServers !== this.iceServers) {
this.iceServers = currentIceServers;
}
});
}
}
Loading