diff --git a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.spec.tsx b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.spec.tsx new file mode 100644 index 0000000000000..78de9bcf37067 --- /dev/null +++ b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.spec.tsx @@ -0,0 +1,146 @@ +import type { IUser, IRoom } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { useMediaCallAction } from '@rocket.chat/ui-voip'; +import { act, renderHook } from '@testing-library/react'; + +import { useMediaCallRoomAction } from './useMediaCallRoomAction'; +import FakeRoomProvider from '../../../tests/mocks/client/FakeRoomProvider'; +import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + ...jest.requireActual('@rocket.chat/ui-contexts'), + useUserAvatarPath: jest.fn((_args: any) => 'avatar-url'), +})); + +jest.mock('@rocket.chat/ui-voip', () => ({ + useMediaCallAction: jest.fn(), +})); + +const getUserInfoMocked = jest.fn().mockResolvedValue({ user: createFakeUser({ _id: 'peer-uid', username: 'peer-username' }) }); + +const appRoot = (overrides: { user?: IUser | null; room?: IRoom; subscription?: SubscriptionWithRoom } = {}) => { + const { + user = createFakeUser({ _id: 'own-uid', username: 'own-username' }), + room = createFakeRoom({ uids: ['own-uid', 'peer-uid'] }), + subscription = createFakeSubscription(), + } = overrides; + + const root = mockAppRoot() + .withRoom(room) + .withEndpoint('GET', '/v1/users.info', getUserInfoMocked) + .wrap((children) => ( + + {children} + + )); + + if (user !== null) { + root.withUser(user); + } + + return root.build(); +}; + +describe('useMediaCallRoomAction', () => { + const useMediaCallActionMocked = jest.mocked(useMediaCallAction); + + beforeEach(() => { + jest.clearAllMocks(); + + useMediaCallActionMocked.mockReturnValue({ + action: jest.fn(), + title: 'Start_call', + icon: 'phone', + }); + }); + + it('should return undefined if ownUserId is not defined', () => { + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot({ user: null }), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if there are no other users in the room', () => { + const fakeRoom = createFakeRoom({ uids: ['own-uid'] }); + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot({ room: fakeRoom }), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if there are more than one other user (Group DM)', () => { + const fakeRoom = createFakeRoom({ uids: ['own-uid', 'peer-uid-1', 'peer-uid-2'] }); + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot({ room: fakeRoom }), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if callAction is undefined', () => { + useMediaCallActionMocked.mockReturnValue(undefined); + + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot(), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if subscription is blocked', () => { + const fakeBlockedSubscription = createFakeSubscription({ blocker: false, blocked: true }); + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot({ subscription: fakeBlockedSubscription }), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if subscription is blocker', () => { + const fakeBlockedSubscription = createFakeSubscription({ blocked: false, blocker: true }); + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot({ subscription: fakeBlockedSubscription }), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if room is federated', () => { + const fakeFederatedRoom = createFakeRoom({ uids: ['own-uid', 'peer-uid'], federated: true }); + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot({ room: fakeFederatedRoom }), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return the action config if all conditions are met', () => { + const actionMock = jest.fn(); + useMediaCallActionMocked.mockReturnValue({ + action: actionMock, + title: 'Start_call', + icon: 'phone', + }); + + const { result } = renderHook(() => useMediaCallRoomAction(), { + wrapper: appRoot(), + }); + + expect(result.current).toEqual({ + id: 'start-voice-call', + title: 'Start_call', + icon: 'phone', + featured: true, + action: expect.any(Function), + groups: ['direct'], + }); + + // Test the action trigger + act(() => result.current?.action?.()); + expect(actionMock).toHaveBeenCalled(); + }); +}); diff --git a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts index d0e458662251c..a4652eebf2fdd 100644 --- a/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useMediaCallRoomAction.ts @@ -1,3 +1,4 @@ +import { isRoomFederated } from '@rocket.chat/core-typings'; import { useUserAvatarPath, useUserId } from '@rocket.chat/ui-contexts'; import type { TranslationKey, RoomToolboxActionConfig } from '@rocket.chat/ui-contexts'; import type { PeerInfo } from '@rocket.chat/ui-voip'; @@ -23,9 +24,11 @@ const getPeerId = (uids: string[], ownUserId: string | undefined) => { }; export const useMediaCallRoomAction = () => { - const { uids = [] } = useRoom(); + const room = useRoom(); + const { uids = [] } = room; const subscription = useRoomSubscription(); const ownUserId = useUserId(); + const federated = isRoomFederated(room); const getAvatarUrl = useUserAvatarPath(); @@ -52,7 +55,7 @@ export const useMediaCallRoomAction = () => { const blocked = subscription?.blocked || subscription?.blocker; return useMemo((): RoomToolboxActionConfig | undefined => { - if (!peerId || !callAction || blocked) { + if (!peerId || !callAction || blocked || federated) { return undefined; } @@ -66,5 +69,5 @@ export const useMediaCallRoomAction = () => { action: () => action(), groups: ['direct'] as const, }; - }, [peerId, callAction, blocked]); + }, [peerId, callAction, blocked, federated]); }; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx new file mode 100644 index 0000000000000..68a16d072ee68 --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx @@ -0,0 +1,142 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { useMediaCallContext } from '@rocket.chat/ui-voip'; +import { act, renderHook } from '@testing-library/react'; + +import { useUserMediaCallAction } from './useUserMediaCallAction'; +import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../../../tests/mocks/data'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + ...jest.requireActual('@rocket.chat/ui-contexts'), + useUserAvatarPath: jest.fn().mockReturnValue((_args: any) => 'avatar-url'), + useUserCard: jest.fn().mockReturnValue({ closeUserCard: jest.fn() }), +})); + +jest.mock('@rocket.chat/ui-voip', () => ({ + ...jest.requireActual('@rocket.chat/ui-voip'), + useMediaCallContext: jest.fn().mockImplementation(() => ({ + state: 'closed', + onToggleWidget: jest.fn(), + })), +})); + +const useMediaCallContextMocked = jest.mocked(useMediaCallContext); + +describe('useUserMediaCallAction', () => { + const fakeUser = createFakeUser({ _id: 'own-uid' }); + const mockRid = 'room-id'; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return undefined if room is federated', () => { + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { + wrapper: mockAppRoot() + .withJohnDoe() + .withRoom(createFakeRoom({ federated: true })) + .build(), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if state is unauthorized', () => { + useMediaCallContextMocked.mockReturnValueOnce({ + state: 'unauthorized', + onToggleWidget: undefined, + onEndCall: undefined, + peerInfo: undefined, + }); + + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { wrapper: mockAppRoot().build() }); + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if subscription is blocked', () => { + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { + wrapper: mockAppRoot() + .withJohnDoe() + .withRoom(createFakeRoom()) + .withSubscription(createFakeSubscription({ blocker: false, blocked: true })) + .build(), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if subscription is blocker', () => { + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { + wrapper: mockAppRoot() + .withJohnDoe() + .withRoom(createFakeRoom()) + .withSubscription(createFakeSubscription({ blocker: true, blocked: false })) + .build(), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return undefined if user is own user', () => { + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { + wrapper: mockAppRoot().withUser(fakeUser).withRoom(createFakeRoom()).withSubscription(createFakeSubscription()).build(), + }); + + expect(result.current).toBeUndefined(); + }); + + it('should return action if conditions are met', () => { + const fakeUser = createFakeUser(); + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { + wrapper: mockAppRoot() + .withJohnDoe() + .withRoom(createFakeRoom()) + .withSubscription(createFakeSubscription()) + .withTranslations('en', 'core', { + Voice_call__user_: 'Voice call {{user}}', + }) + .build(), + }); + + expect(result.current).toEqual( + expect.objectContaining({ + type: 'communication', + icon: 'phone', + title: `Voice call ${fakeUser.name}`, + disabled: false, + }), + ); + }); + + it('should call onClick handler correctly', () => { + const mockOnToggleWidget = jest.fn(); + useMediaCallContextMocked.mockReturnValueOnce({ + state: 'closed', + onToggleWidget: mockOnToggleWidget, + peerInfo: undefined, + onEndCall: () => undefined, + }); + + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid)); + + act(() => result.current?.onClick()); + + expect(mockOnToggleWidget).toHaveBeenCalledWith({ + userId: fakeUser._id, + displayName: fakeUser.name, + avatarUrl: 'avatar-url', + }); + }); + + it('should be disabled if state is not closed, new, or unlicensed', () => { + useMediaCallContextMocked.mockReturnValueOnce({ + state: 'calling', + onToggleWidget: jest.fn(), + peerInfo: undefined, + onEndCall: () => undefined, + }); + + const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid)); + + expect(result.current?.disabled).toBe(true); + }); +}); diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts index cbf6341644743..dfcb2a7a02e2b 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.ts @@ -1,5 +1,6 @@ +import { isRoomFederated } from '@rocket.chat/core-typings'; import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard } from '@rocket.chat/ui-contexts'; +import { useUserAvatarPath, useUserId, useUserSubscription, useUserCard, useUserRoom } from '@rocket.chat/ui-contexts'; import { useMediaCallContext } from '@rocket.chat/ui-voip'; import { useTranslation } from 'react-i18next'; @@ -13,9 +14,14 @@ export const useUserMediaCallAction = (user: Pick