diff --git a/src/components/Buttons/EndCallButton/EndCallButton.test.tsx b/src/components/Buttons/EndCallButton/EndCallButton.test.tsx index faf7769e0..940dd6afe 100644 --- a/src/components/Buttons/EndCallButton/EndCallButton.test.tsx +++ b/src/components/Buttons/EndCallButton/EndCallButton.test.tsx @@ -7,56 +7,14 @@ const mockVideoContext = { room: { disconnect: jest.fn(), }, - isSharingScreen: false, - toggleScreenShare: jest.fn(), - removeLocalAudioTrack: jest.fn(), - removeLocalVideoTrack: jest.fn(), }; jest.mock('../../../hooks/useVideoContext/useVideoContext', () => () => mockVideoContext); describe('End Call button', () => { - describe('when it is clicked', () => { - describe('while sharing screen', () => { - let wrapper; - - beforeAll(() => { - jest.clearAllMocks(); - mockVideoContext.isSharingScreen = true; - wrapper = shallow(); - wrapper.simulate('click'); - }); - - it('should stop local audio tracks', () => { - expect(mockVideoContext.removeLocalAudioTrack).toHaveBeenCalled(); - }); - - it('should stop local video tracks', () => { - expect(mockVideoContext.removeLocalVideoTrack).toHaveBeenCalled(); - }); - - it('should toggle screen sharing off', () => { - expect(mockVideoContext.toggleScreenShare).toHaveBeenCalled(); - }); - - it('should disconnect from the room ', () => { - expect(mockVideoContext.room.disconnect).toHaveBeenCalled(); - }); - }); - - describe('while not sharing screen', () => { - let wrapper; - - beforeAll(() => { - jest.clearAllMocks(); - mockVideoContext.isSharingScreen = false; - wrapper = shallow(); - wrapper.simulate('click'); - }); - - it('should not toggle screen sharing', () => { - expect(mockVideoContext.toggleScreenShare).not.toHaveBeenCalled(); - }); - }); + it('should disconnect from the room when clicked', () => { + const wrapper = shallow(); + wrapper.simulate('click'); + expect(mockVideoContext.room.disconnect).toHaveBeenCalled(); }); }); diff --git a/src/components/Buttons/EndCallButton/EndCallButton.tsx b/src/components/Buttons/EndCallButton/EndCallButton.tsx index d34e60daf..d5d9bf76e 100644 --- a/src/components/Buttons/EndCallButton/EndCallButton.tsx +++ b/src/components/Buttons/EndCallButton/EndCallButton.tsx @@ -20,19 +20,10 @@ const useStyles = makeStyles((theme: Theme) => export default function EndCallButton(props: { className?: string }) { const classes = useStyles(); - const { room, isSharingScreen, toggleScreenShare, removeLocalAudioTrack, removeLocalVideoTrack } = useVideoContext(); - - const handleClick = () => { - if (isSharingScreen) { - toggleScreenShare(); - } - removeLocalAudioTrack(); - removeLocalVideoTrack(); - room!.disconnect(); - }; + const { room } = useVideoContext(); return ( - ); diff --git a/src/components/VideoProvider/index.test.tsx b/src/components/VideoProvider/index.test.tsx index e6b4192f3..1706452df 100644 --- a/src/components/VideoProvider/index.test.tsx +++ b/src/components/VideoProvider/index.test.tsx @@ -5,7 +5,7 @@ import { Room, TwilioError } from 'twilio-video'; import { VideoProvider } from './index'; import useLocalTracks from './useLocalTracks/useLocalTracks'; import useRoom from './useRoom/useRoom'; -import useHandleRoomDisconnectionErrors from './useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors'; +import useHandleRoomDisconnection from './useHandleRoomDisconnection/useHandleRoomDisconnection'; import useHandleTrackPublicationFailed from './useHandleTrackPublicationFailed/useHandleTrackPublicationFailed'; import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; @@ -17,10 +17,11 @@ jest.mock('./useLocalTracks/useLocalTracks', () => getLocalVideoTrack: () => {}, getLocalAudioTrack: () => {}, isAcquiringLocalTracks: true, + removeLocalAudioTrack: () => {}, removeLocalVideoTrack: () => {}, })) ); -jest.mock('./useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors'); +jest.mock('./useHandleRoomDisconnection/useHandleRoomDisconnection'); jest.mock('./useHandleTrackPublicationFailed/useHandleTrackPublicationFailed'); describe('the VideoProvider component', () => { @@ -47,7 +48,14 @@ describe('the VideoProvider component', () => { dominantSpeaker: true, }); expect(useLocalTracks).toHaveBeenCalled(); - expect(useHandleRoomDisconnectionErrors).toHaveBeenCalledWith(mockRoom, expect.any(Function)); + expect(useHandleRoomDisconnection).toHaveBeenCalledWith( + mockRoom, + expect.any(Function), + expect.any(Function), + expect.any(Function), + false, + expect.any(Function) + ); expect(useHandleTrackPublicationFailed).toHaveBeenCalledWith(mockRoom, expect.any(Function)); }); diff --git a/src/components/VideoProvider/index.tsx b/src/components/VideoProvider/index.tsx index 76827f8fc..8d29a1402 100644 --- a/src/components/VideoProvider/index.tsx +++ b/src/components/VideoProvider/index.tsx @@ -11,7 +11,7 @@ import { ErrorCallback } from '../../types'; import { SelectedParticipantProvider } from './useSelectedParticipant/useSelectedParticipant'; import AttachVisibilityHandler from './AttachVisibilityHandler/AttachVisibilityHandler'; -import useHandleRoomDisconnectionErrors from './useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors'; +import useHandleRoomDisconnection from './useHandleRoomDisconnection/useHandleRoomDisconnection'; import useHandleTrackPublicationFailed from './useHandleTrackPublicationFailed/useHandleTrackPublicationFailed'; import useLocalTracks from './useLocalTracks/useLocalTracks'; import useRoom from './useRoom/useRoom'; @@ -33,7 +33,6 @@ export interface IVideoContext { getLocalVideoTrack: (newOptions?: CreateLocalTrackOptions) => Promise; getLocalAudioTrack: (deviceId?: string) => Promise; isAcquiringLocalTracks: boolean; - removeLocalAudioTrack: () => void; removeLocalVideoTrack: () => void; isSharingScreen: boolean; toggleScreenShare: () => void; @@ -65,11 +64,19 @@ export function VideoProvider({ options, children, onError = () => {} }: VideoPr } = useLocalTracks(); const { room, isConnecting, connect } = useRoom(localTracks, onErrorCallback, options); - // Register onError callback functions. - useHandleRoomDisconnectionErrors(room, onError); - useHandleTrackPublicationFailed(room, onError); const [isSharingScreen, toggleScreenShare] = useScreenShareToggle(room, onError); + // Register callback functions to be called on room disconnect. + useHandleRoomDisconnection( + room, + onError, + removeLocalAudioTrack, + removeLocalVideoTrack, + isSharingScreen, + toggleScreenShare + ); + useHandleTrackPublicationFailed(room, onError); + return ( {} }: VideoPr getLocalAudioTrack, connect, isAcquiringLocalTracks, - removeLocalAudioTrack, removeLocalVideoTrack, isSharingScreen, toggleScreenShare, diff --git a/src/components/VideoProvider/useHandleRoomDisconnection/useHandleRoomDisconnection.test.tsx b/src/components/VideoProvider/useHandleRoomDisconnection/useHandleRoomDisconnection.test.tsx new file mode 100644 index 000000000..742c6ad03 --- /dev/null +++ b/src/components/VideoProvider/useHandleRoomDisconnection/useHandleRoomDisconnection.test.tsx @@ -0,0 +1,140 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { Room } from 'twilio-video'; +import EventEmitter from 'events'; +import useHandleRoomDisconnection from './useHandleRoomDisconnection'; + +const mockOnError = jest.fn(); +const mockRemoveLocalAudioTrack = jest.fn(); +const mockRemoveLocalVideoTrack = jest.fn(); +const mockToggleScreenSharing = jest.fn(); + +describe('the useHandleRoomDisconnection hook', () => { + let mockRoom: any = new EventEmitter(); + + beforeEach(jest.clearAllMocks); + + it('should do nothing if the room emits a "disconnected" event with no error', () => { + renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + false, + mockToggleScreenSharing + ) + ); + act(() => { + mockRoom.emit('disconnected', mockRoom); + }); + expect(mockOnError).not.toHaveBeenCalled(); + }); + + it('should react to the rooms "disconnected" event and invoke onError callback if there is an error', () => { + renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + false, + mockToggleScreenSharing + ) + ); + act(() => { + mockRoom.emit('disconnected', mockRoom, 'mockError'); + }); + expect(mockOnError).toHaveBeenCalledWith('mockError'); + }); + + it('should remove local tracks when the "disconnected" event is emitted', () => { + renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + false, + mockToggleScreenSharing + ) + ); + act(() => { + mockRoom.emit('disconnected', mockRoom, 'mockError'); + }); + expect(mockRemoveLocalAudioTrack).toHaveBeenCalled(); + expect(mockRemoveLocalVideoTrack).toHaveBeenCalled(); + }); + + it('should not toggle screensharing when the "disconnected" event is emitted and isSharing is false', () => { + renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + false, + mockToggleScreenSharing + ) + ); + act(() => { + mockRoom.emit('disconnected', mockRoom, 'mockError'); + }); + expect(mockToggleScreenSharing).not.toHaveBeenCalled(); + }); + + it('should toggle screensharing when the "disconnected" event is emitted and isSharing is true', () => { + renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + true, + mockToggleScreenSharing + ) + ); + act(() => { + mockRoom.emit('disconnected', mockRoom, 'mockError'); + }); + expect(mockToggleScreenSharing).toHaveBeenCalled(); + }); + + it('should tear down old listeners when receiving a new room', () => { + const originalMockRoom = mockRoom; + const { rerender } = renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + false, + mockToggleScreenSharing + ) + ); + expect(originalMockRoom.listenerCount('disconnected')).toBe(1); + + act(() => { + mockRoom = new EventEmitter() as Room; + }); + + rerender(); + + expect(originalMockRoom.listenerCount('disconnected')).toBe(0); + expect(mockRoom.listenerCount('disconnected')).toBe(1); + }); + + it('should clean up listeners on unmount', () => { + const { unmount } = renderHook(() => + useHandleRoomDisconnection( + mockRoom, + mockOnError, + mockRemoveLocalAudioTrack, + mockRemoveLocalVideoTrack, + false, + mockToggleScreenSharing + ) + ); + unmount(); + expect(mockRoom.listenerCount('disconnected')).toBe(0); + }); +}); diff --git a/src/components/VideoProvider/useHandleRoomDisconnection/useHandleRoomDisconnection.ts b/src/components/VideoProvider/useHandleRoomDisconnection/useHandleRoomDisconnection.ts new file mode 100644 index 000000000..7afe6756f --- /dev/null +++ b/src/components/VideoProvider/useHandleRoomDisconnection/useHandleRoomDisconnection.ts @@ -0,0 +1,34 @@ +import { Room, TwilioError } from 'twilio-video'; +import { useEffect } from 'react'; + +import { Callback } from '../../../types'; + +export default function useHandleRoomDisconnection( + room: Room | null, + onError: Callback, + removeLocalAudioTrack: () => void, + removeLocalVideoTrack: () => void, + isSharingScreen: boolean, + toggleScreenShare: () => void +) { + useEffect(() => { + if (room) { + const onDisconnected = (_: Room, error: TwilioError) => { + if (error) { + onError(error); + } + + removeLocalAudioTrack(); + removeLocalVideoTrack(); + if (isSharingScreen) { + toggleScreenShare(); + } + }; + + room.on('disconnected', onDisconnected); + return () => { + room.off('disconnected', onDisconnected); + }; + } + }, [room, onError, removeLocalAudioTrack, removeLocalVideoTrack, isSharingScreen, toggleScreenShare]); +} diff --git a/src/components/VideoProvider/useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors.test.tsx b/src/components/VideoProvider/useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors.test.tsx deleted file mode 100644 index 926a1e0aa..000000000 --- a/src/components/VideoProvider/useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { act, renderHook } from '@testing-library/react-hooks'; -import EventEmitter from 'events'; - -import useHandleRoomDisconnectionErrors from './useHandleRoomDisconnectionErrors'; -import { Room } from 'twilio-video'; - -describe('the useHandleRoomDisconnectionErrors hook', () => { - let mockRoom: any = new EventEmitter(); - - it('should do nothing if the room emits a "disconnected" event with no error', () => { - const mockOnError = jest.fn(); - renderHook(() => useHandleRoomDisconnectionErrors(mockRoom, mockOnError)); - act(() => { - mockRoom.emit('disconnected', 'disconnected'); - }); - expect(mockOnError).not.toHaveBeenCalled(); - }); - - it('should react to the rooms "disconnected" event and invoke onError callback if there is an error', () => { - const mockOnError = jest.fn(); - renderHook(() => useHandleRoomDisconnectionErrors(mockRoom, mockOnError)); - act(() => { - mockRoom.emit('disconnected', 'disconnected', 'mockError'); - }); - expect(mockOnError).toHaveBeenCalledWith('mockError'); - }); - - it('should tear down old listeners when receiving a new room', () => { - const originalMockRoom = mockRoom; - const { rerender } = renderHook(() => useHandleRoomDisconnectionErrors(mockRoom, () => {})); - expect(originalMockRoom.listenerCount('disconnected')).toBe(1); - - act(() => { - mockRoom = new EventEmitter() as Room; - }); - - rerender(); - - expect(originalMockRoom.listenerCount('disconnected')).toBe(0); - expect(mockRoom.listenerCount('disconnected')).toBe(1); - }); - - it('should clean up listeners on unmount', () => { - const { unmount } = renderHook(() => useHandleRoomDisconnectionErrors(mockRoom, () => {})); - unmount(); - expect(mockRoom.listenerCount('disconnected')).toBe(0); - }); -}); diff --git a/src/components/VideoProvider/useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors.ts b/src/components/VideoProvider/useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors.ts deleted file mode 100644 index 78f5e16ed..000000000 --- a/src/components/VideoProvider/useHandleRoomDisconnectionErrors/useHandleRoomDisconnectionErrors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Room, TwilioError } from 'twilio-video'; -import { useEffect } from 'react'; - -import { Callback } from '../../../types'; - -export default function useHandleRoomDisconnectionErrors(room: Room | null, onError: Callback) { - useEffect(() => { - if (room) { - const onDisconnected = (_: Room, error: TwilioError) => { - if (error) { - onError(error); - } - }; - - room.on('disconnected', onDisconnected); - return () => { - room.off('disconnected', onDisconnected); - }; - } - }, [room, onError]); -}