From 54274874cfa5800032333dcd9968cc9015ac0a65 Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 14:22:36 +0900 Subject: [PATCH 1/7] =?UTF-8?q?style:=20popup=20=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=EA=B3=B3=EC=97=90=EC=84=9C=20closeOverlay=20?= =?UTF-8?q?->=20closePopup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/business/hooks/usePopup/usePasswordPopup.tsx | 7 ++++--- .../src/business/hooks/useWebRTC/useSignalingSocket.ts | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/business/hooks/usePopup/usePasswordPopup.tsx b/frontend/src/business/hooks/usePopup/usePasswordPopup.tsx index c597aa84..8f081989 100644 --- a/frontend/src/business/hooks/usePopup/usePasswordPopup.tsx +++ b/frontend/src/business/hooks/usePopup/usePasswordPopup.tsx @@ -6,7 +6,7 @@ import { randomString } from '@utils/ramdom'; type openPasswordPopupParams = { host?: boolean; - onSubmit?: ({ password, closePopup }: { password: string; closePopup: () => void }) => void; + onSubmit?: ({ password, closePopup }: { password: string } & ClosePopupFunc) => void; onCancel?: () => void; }; @@ -16,12 +16,13 @@ export function usePasswordPopup() { const openPasswordPopup = ({ host, onSubmit, onCancel }: openPasswordPopupParams) => { const defaultValue = host ? randomString() : ''; - open(({ close: closePopup }) => ( + openOverlay(({ closeOverlay: closePopup }) => ( { onSubmit?.({ password, closePopup }); + onSubmit?.({ password, closePopup }); }} defaultValue={defaultValue} /> diff --git a/frontend/src/business/hooks/useWebRTC/useSignalingSocket.ts b/frontend/src/business/hooks/useWebRTC/useSignalingSocket.ts index 935c347d..9eab594d 100644 --- a/frontend/src/business/hooks/useWebRTC/useSignalingSocket.ts +++ b/frontend/src/business/hooks/useWebRTC/useSignalingSocket.ts @@ -20,7 +20,8 @@ export function useSignalingSocket() { openPasswordPopup({ host: true, onCancel, - onSubmit: ({ password, closePopup }) => initHostSocketEvents({ password, closePopup, roomName, onSuccess }), + onSubmit: ({ password, closePopup }) => + initHostSocketEvents({ password, closePopup: closePopup, roomName, onSuccess }), }); }; From bc791bc0d1fb7fb536c10eefddb030a42f0126e2 Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 17:33:31 +0900 Subject: [PATCH 2/7] =?UTF-8?q?style:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EB=A6=84=20=EC=9E=98=EB=AA=BB=20=EB=90=98?= =?UTF-8?q?=EC=96=B4=EC=9E=88=EB=8D=98=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChattingPage -> SettingPage --- frontend/src/pages/HumanChatPage/SettingPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/HumanChatPage/SettingPage.tsx b/frontend/src/pages/HumanChatPage/SettingPage.tsx index d4da3c6c..e127fb32 100644 --- a/frontend/src/pages/HumanChatPage/SettingPage.tsx +++ b/frontend/src/pages/HumanChatPage/SettingPage.tsx @@ -10,7 +10,7 @@ import type { OutletContext } from './HumanChatPage'; import { useSettingPageMediaOptinos } from './useSettingPageMediaOptions'; import { useSettingPageProfileNicknameSetting } from './useSettingPageProfileNicknameSetting'; -export default function ChattingPage() { +export default function SettingPage() { const socketManager = HumanSocketManager.getInstance(); const navigate = useNavigate(); From cbfa34c7a6ec7820c6e6ae8c2d1df1233e3f807b Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 17:35:18 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=EA=B0=80?= =?UTF-8?q?=20=EB=93=A4=EC=96=B4=EC=98=A4=EA=B8=B0=20=EC=A0=84=EC=97=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84,=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=B4=EB=8F=84=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - zustand의 상태는 리액트 컴포넌트 안에서만 유효함 - 만약 컴포넌트 밖에서 사용하려면 store.getState() 로 최신 상태를 가져올 수 있음 --- .../useDataChannel.eventListeners.ts | 76 ----------------- .../hooks/useWebRTC/useDataChannel.ts | 40 +++------ .../useWebRTC/useDataChannelEventListener.ts | 85 +++++++++++++++++++ 3 files changed, 99 insertions(+), 102 deletions(-) delete mode 100644 frontend/src/business/hooks/useWebRTC/useDataChannel.eventListeners.ts create mode 100644 frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.ts diff --git a/frontend/src/business/hooks/useWebRTC/useDataChannel.eventListeners.ts b/frontend/src/business/hooks/useWebRTC/useDataChannel.eventListeners.ts deleted file mode 100644 index b23de177..00000000 --- a/frontend/src/business/hooks/useWebRTC/useDataChannel.eventListeners.ts +++ /dev/null @@ -1,76 +0,0 @@ -import WebRTC from '@business/services/__mocks__/WebRTC'; - -import { ProfileInfo } from '@stores/zustandStores/useProfileInfo'; - -import { array2ArrayBuffer } from '@utils/array'; - -const webRTC = WebRTC.getInstance(); - -export async function setMediaStates({ - ev: { data }, - setRemoteMicOn, - setRemoteVideoOn, -}: { - ev: MessageEvent; - setRemoteMicOn: (onOrOff: boolean) => void; - setRemoteVideoOn: (onOrOff: boolean) => void; -}) { - const mediaInfoArray = JSON.parse(data); - - mediaInfoArray.forEach(({ type, onOrOff }: { type: string; onOrOff: boolean }) => { - if (type === 'audio') { - setRemoteMicOn(onOrOff); - } else if (type === 'video') { - setRemoteVideoOn(onOrOff); - } - }); -} - -export function setRemoteProfileImageState({ - ev: { data }, - setRemoteProfileImage, -}: { - ev: MessageEvent; - setRemoteProfileImage: (profileInfo: ProfileInfo) => void; -}) { - const receivedData = JSON.parse(data); - - const { type, arrayBuffer: array } = receivedData; - - const arrayBuffer = array2ArrayBuffer(array); - - setRemoteProfileImage({ arrayBuffer, type }); -} - -export function setRemoteNicknameState({ - ev: { data }, - setRemoteNickname, -}: { - ev: MessageEvent; - setRemoteNickname: (nickname: string) => void; -}) { - setRemoteNickname(data); -} - -export function sendNowMediaStates(this: RTCDataChannel) { - const audioTrack = webRTC.getFirstAudioTrack(); - const videoTrack = webRTC.getFirstVideoTrack(); - - this.send( - JSON.stringify([ - { type: 'audio', onOrOff: audioTrack?.enabled }, - { type: 'video', onOrOff: videoTrack?.enabled }, - ]), - ); -} - -export function sendMyProfileImage(this: RTCDataChannel, { myProfile }: { myProfile: ProfileInfo | undefined }) { - this.send(JSON.stringify({ myProfile })); -} - -export function sendMyNickname(this: RTCDataChannel, { myNickname }: { myNickname: string | undefined }) { - if (!myNickname) { - return; - } - this.send(myNickname); -} diff --git a/frontend/src/business/hooks/useWebRTC/useDataChannel.ts b/frontend/src/business/hooks/useWebRTC/useDataChannel.ts index 7561e635..dd4fedcc 100644 --- a/frontend/src/business/hooks/useWebRTC/useDataChannel.ts +++ b/frontend/src/business/hooks/useWebRTC/useDataChannel.ts @@ -1,34 +1,22 @@ import WebRTC from '@business/services/WebRTC'; -import { useMediaInfo } from '@stores/zustandStores/useMediaInfo'; -import { useProfileInfo } from '@stores/zustandStores/useProfileInfo'; - -import { - sendMyNickname, - sendMyProfileImage, - sendNowMediaStates, - setMediaStates, - setRemoteNicknameState, - setRemoteProfileImageState, -} from './useDataChannel.eventListeners'; +import { useDataChannelEventListener } from './useDataChannelEventListener'; export function useDataChannel() { - const { setRemoteMicOn, setRemoteVideoOn } = useMediaInfo(state => ({ - setRemoteMicOn: state.setRemoteMicOn, - setRemoteVideoOn: state.setRemoteVideoOn, - })); - const { myNickname, myProfile, setRemoteNickname, setRemoteProfileImage } = useProfileInfo(state => ({ - setRemoteNickname: state.setRemoteNickname, - setRemoteProfileImage: state.setRemoteProfile, - myNickname: state.myNickname, - myProfile: state.myProfile, - })); const webRTC = WebRTC.getInstance(); + const { + sendMyNickname, + sendMyProfileImage, + sendNowMediaStates, + setMediaStates, + setRemoteNicknameState, + setRemoteProfileImageState, + } = useDataChannelEventListener(); const initMediaInfoChannel = () => { const mediaInfoChannel = webRTC.addDataChannel('mediaInfoChannel'); - mediaInfoChannel?.addEventListener('message', ev => setMediaStates({ ev, setRemoteMicOn, setRemoteVideoOn })); + mediaInfoChannel?.addEventListener('message', ev => setMediaStates({ ev })); mediaInfoChannel?.addEventListener('open', sendNowMediaStates); }; @@ -40,17 +28,17 @@ export function useDataChannel() { const initProfileChannel = () => { const profileChannel = webRTC.addDataChannel('profileChannel'); - profileChannel?.addEventListener('message', ev => setRemoteProfileImageState({ ev, setRemoteProfileImage })); + profileChannel?.addEventListener('message', ev => setRemoteProfileImageState({ ev })); - profileChannel?.addEventListener('open', sendMyProfileImage.bind(profileChannel, { myProfile })); + profileChannel?.addEventListener('open', sendMyProfileImage.bind(profileChannel)); }; const initNicknameChannel = () => { const nicknameChannel = webRTC.addDataChannel('nicknameChannel'); - nicknameChannel?.addEventListener('message', ev => setRemoteNicknameState({ ev, setRemoteNickname })); + nicknameChannel?.addEventListener('message', ev => setRemoteNicknameState({ ev })); - nicknameChannel?.addEventListener('open', sendMyNickname.bind(nicknameChannel, { myNickname })); + nicknameChannel?.addEventListener('open', sendMyNickname.bind(nicknameChannel)); }; const initDataChannels = () => { diff --git a/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.ts b/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.ts new file mode 100644 index 00000000..227cb9bb --- /dev/null +++ b/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.ts @@ -0,0 +1,85 @@ +import { HumanSocketManager } from '@business/services/SocketManager'; +import WebRTC from '@business/services/WebRTC'; + +import { useMediaInfo } from '@stores/zustandStores/useMediaInfo'; +import { useProfileInfo } from '@stores/zustandStores/useProfileInfo'; + +import { array2ArrayBuffer } from '@utils/array'; + +import { DEFAULT_NICKNAME } from '@constants/nickname'; + +export function useDataChannelEventListener() { + const webRTC = WebRTC.getInstance(HumanSocketManager.getInstance()); + + const { setRemoteMicOn, setRemoteVideoOn } = useMediaInfo(state => ({ + setRemoteMicOn: state.setRemoteMicOn, + setRemoteVideoOn: state.setRemoteVideoOn, + })); + const { setRemoteNickname, setRemoteProfileImage } = useProfileInfo(state => ({ + setRemoteProfileImage: state.setRemoteProfile, + setRemoteNickname: state.setRemoteNickname, + })); + + async function setMediaStates({ ev: { data } }: { ev: MessageEvent }) { + const mediaInfoArray = JSON.parse(data); + + mediaInfoArray.forEach(({ type, onOrOff }: { type: string; onOrOff: boolean }) => { + if (type === 'audio') { + setRemoteMicOn(onOrOff); + } else if (type === 'video') { + setRemoteVideoOn(onOrOff); + } + }); + } + + function setRemoteProfileImageState({ ev: { data } }: { ev: MessageEvent }) { + const receivedData = JSON.parse(data); + + const { type, arrayBuffer: array } = receivedData; + + const arrayBuffer = array2ArrayBuffer(array); + + setRemoteProfileImage({ arrayBuffer, type }); + } + + function setRemoteNicknameState({ ev: { data } }: { ev: MessageEvent }) { + setRemoteNickname(data); + } + + function sendNowMediaStates(this: RTCDataChannel) { + const audioTrack = webRTC.getFirstAudioTrack(); + const videoTrack = webRTC.getFirstVideoTrack(); + + this.send( + JSON.stringify([ + { type: 'audio', onOrOff: audioTrack?.enabled }, + { type: 'video', onOrOff: videoTrack?.enabled }, + ]), + ); + } + + function sendMyProfileImage(this: RTCDataChannel) { + const { myProfile } = useProfileInfo.getState(); + if (!myProfile) { + return; + } + this.send(JSON.stringify({ myProfile })); + } + + function sendMyNickname(this: RTCDataChannel) { + const { myNickname } = useProfileInfo.getState(); + if (!myNickname) { + return; + } + this.send(myNickname ?? DEFAULT_NICKNAME.OTHER); + } + + return { + setMediaStates, + setRemoteProfileImageState, + setRemoteNicknameState, + sendNowMediaStates, + sendMyProfileImage, + sendMyNickname, + }; +} From c262ce1dd4be2a56fc06c870d550eb29b2c4cdd9 Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 17:35:40 +0900 Subject: [PATCH 4/7] =?UTF-8?q?style:=20=EA=B8=B0=EB=B3=B8=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=EC=83=81=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/CamContainer/CamContainer.tsx | 6 ++++-- frontend/src/components/ProfileSetting/ProfileSetting.tsx | 4 +++- frontend/src/constants/nickname.ts | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 frontend/src/constants/nickname.ts diff --git a/frontend/src/components/CamContainer/CamContainer.tsx b/frontend/src/components/CamContainer/CamContainer.tsx index cd55b213..45cdf8ce 100644 --- a/frontend/src/components/CamContainer/CamContainer.tsx +++ b/frontend/src/components/CamContainer/CamContainer.tsx @@ -5,6 +5,8 @@ import { useHost } from '@stores/zustandStores/useHost'; import { useMediaInfo } from '@stores/zustandStores/useMediaInfo'; import { useProfileInfo } from '@stores/zustandStores/useProfileInfo'; +import { DEFAULT_NICKNAME } from '@constants/nickname'; + interface CamContainerProps { localVideoRef: React.RefObject; remoteVideoRef: React.RefObject; @@ -50,7 +52,7 @@ export default function CamContainer({ defaultImage="bg-ddung" profileInfo={myProfile} nickname={myNickname} - defaultNickname="나" + defaultNickname={DEFAULT_NICKNAME.ME} />
diff --git a/frontend/src/components/ProfileSetting/ProfileSetting.tsx b/frontend/src/components/ProfileSetting/ProfileSetting.tsx index d93cb2bf..f04e9cd6 100644 --- a/frontend/src/components/ProfileSetting/ProfileSetting.tsx +++ b/frontend/src/components/ProfileSetting/ProfileSetting.tsx @@ -6,6 +6,8 @@ import { SelectOptions } from '@components/Select'; import { useMediaInfo } from '@stores/zustandStores/useMediaInfo'; import { useProfileInfo } from '@stores/zustandStores/useProfileInfo'; +import { DEFAULT_NICKNAME } from '@constants/nickname'; + import DeviceSelect from './DeviceSelect'; import DeviceToggleButtons from './DeviceToggleButtons'; @@ -57,7 +59,7 @@ export default function ProfileSetting({ defaultImage="bg-ddung" profileInfo={myProfile} nickname={myNickname} - defaultNickname="나" + defaultNickname={DEFAULT_NICKNAME.ME} />
diff --git a/frontend/src/constants/nickname.ts b/frontend/src/constants/nickname.ts new file mode 100644 index 00000000..209bd753 --- /dev/null +++ b/frontend/src/constants/nickname.ts @@ -0,0 +1,4 @@ +export const DEFAULT_NICKNAME = { + ME: '나', + OTHER: '상대방', +}; From 189ff42351fd59766bcc5989ac3c055c06ec8a68 Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 17:36:18 +0900 Subject: [PATCH 5/7] =?UTF-8?q?test:=20useDataChannel=20=ED=9B=85=EC=9D=B4?= =?UTF-8?q?=20=EB=91=90=EA=B0=9C=EB=A1=9C=20=EB=B6=84=EB=A6=AC=EB=90=A8?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이벤트 등록하는 부분, 핸들러 부분을 따로 테스트코드 작성함 --- .../hooks/useWebRTC/useDataChannel.spec.ts | 148 ++---------- .../useDataChannelEventListener.spec.ts | 212 ++++++++++++++++++ .../stores/zustandStores/useProfileInfo.ts | 6 +- 3 files changed, 238 insertions(+), 128 deletions(-) create mode 100644 frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts diff --git a/frontend/src/business/hooks/useWebRTC/useDataChannel.spec.ts b/frontend/src/business/hooks/useWebRTC/useDataChannel.spec.ts index a020a270..0752ee83 100644 --- a/frontend/src/business/hooks/useWebRTC/useDataChannel.spec.ts +++ b/frontend/src/business/hooks/useWebRTC/useDataChannel.spec.ts @@ -1,32 +1,19 @@ import { useDataChannel } from '.'; -import { RenderHookResult, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import WebRTC from '@business/services/WebRTC'; -import { - sendMyNickname, - sendMyProfileImage, - sendNowMediaStates, - setMediaStates, - setRemoteNicknameState, - setRemoteProfileImageState, -} from './useDataChannel.eventListeners'; - -vi.mock('@business/services/WebRTC'); - -type RenderHookResultType = { - initDataChannels: () => void; - dataChannels: Map; -}; - describe('useDataChannel 테스트', () => { let mockWebRTCModule = WebRTC.getInstance(); - let renderUtil: RenderHookResult; - let currentRenderResult: RenderHookResultType; function rerenderHook() { - renderUtil = renderHook(() => useDataChannel()); - currentRenderResult = renderUtil.result.current; + const { + rerender, + result: { + current: { dataChannels, initDataChannels }, + }, + } = renderHook(() => useDataChannel()); + return { rerender, dataChannels, initDataChannels }; } const addEventListener = vi.fn(); @@ -36,86 +23,26 @@ describe('useDataChannel 테스트', () => { vi.spyOn(mockWebRTCModule, 'addDataChannel').mockReturnValue(mediaInfoChannelStub); }); - beforeEach(() => { - rerenderHook(); - }); - afterEach(() => { vi.clearAllMocks(); }); - afterAll(() => { - vi.resetAllMocks(); - vi.resetModules(); - }); - - describe('initDataChannels 함수 테스트: 아래 A ~ B의 함수가 실행됨', () => { + describe('initDataChannels 함수 테스트: 아래 A ~ D의 함수가 실행됨', () => { describe('A. initMediaInfoChannel 함수 테스트', () => { it('mediaInfoChannel 데이터 채널 추가 + message와 open 이벤트가 등록됨.', () => { - currentRenderResult.initDataChannels(); + const { initDataChannels } = rerenderHook(); + initDataChannels(); expect(mockWebRTCModule.addDataChannel).toBeCalledWith('mediaInfoChannel'); expect(addEventListener).toBeCalledWith('message', expect.any(Function)); expect(addEventListener).toBeCalledWith('open', expect.any(Function)); }); - - it(`message 이벤트: setMediaStates 호출 - \t 1. type === video라면 onOrOff와 함께 setRemoteVideoOn 호출, 상대 비디오 상태 설정 - \t 2. type === audio라면 onOrOff와 함께 setMicOn 호출, 상대 마이크 상태 설정`, () => { - const testDatas = [ - { data: JSON.stringify([{ type: 'video', onOrOff: true }]) }, - { data: JSON.stringify([{ type: 'audio', onOrOff: true }]) }, - { data: JSON.stringify([{ type: 'video', onOrOff: false }]) }, - { data: JSON.stringify([{ type: 'audio', onOrOff: false }]) }, - ]; - - testDatas.forEach(async (ev: any) => { - const setRemoteMicOn = vi.fn(); - const setRemoteVideoOn = vi.fn(); - const { type, onOrOff } = JSON.parse(ev.data); - - setMediaStates({ ev, setRemoteMicOn, setRemoteVideoOn }); - - if (type === 'audio') { - expect(setRemoteMicOn).toBeCalledWith(onOrOff); - expect(setRemoteVideoOn).not.toBeCalled(); - } - - if (type === 'video') { - expect(setRemoteMicOn).not.toBeCalled(); - expect(setRemoteVideoOn).toBeCalledWith(onOrOff); - } - }); - }); - - it('open 이벤트: sendNowMediaStates 호출, 현재 미디어 상태를 전송', () => { - const testDatas = [ - { audioTrack: { enabled: true }, videoTrack: { enabled: true } }, - { audioTrack: { enabled: false }, videoTrack: { enabled: false } }, - { audioTrack: { enabled: true }, videoTrack: { enabled: false } }, - { audioTrack: { enabled: false }, videoTrack: { enabled: true } }, - ]; - - testDatas.forEach(({ audioTrack, videoTrack }) => { - vi.spyOn(mockWebRTCModule, 'getFirstAudioTrack').mockReturnValueOnce(audioTrack as any); - vi.spyOn(mockWebRTCModule, 'getFirstVideoTrack').mockReturnValueOnce(videoTrack as any); - const RTCDataChannelSendFn = vi.fn(); - - sendNowMediaStates.call({ send: RTCDataChannelSendFn } as any); - - expect(RTCDataChannelSendFn).toBeCalledWith( - JSON.stringify([ - { type: 'audio', onOrOff: audioTrack?.enabled }, - { type: 'video', onOrOff: videoTrack?.enabled }, - ]), - ); - }); - }); }); describe('B. initChatChannel 함수 테스트', () => { it('chatChannel 데이터 채널 추가', () => { - currentRenderResult.initDataChannels(); + const { initDataChannels } = rerenderHook(); + initDataChannels(); expect(mockWebRTCModule.addDataChannel).toBeCalledWith('chatChannel'); }); @@ -123,64 +50,31 @@ describe('useDataChannel 테스트', () => { describe('C. initProfileChannel 함수 테스트', () => { it('profileChannel 데이터 채널 추가 + message와 open 이벤트가 등록됨.', () => { - currentRenderResult.initDataChannels(); + const { initDataChannels } = rerenderHook(); + initDataChannels(); expect(mockWebRTCModule.addDataChannel).toBeCalledWith('profileChannel'); expect(addEventListener).toBeCalledWith('message', expect.any(Function)); expect(addEventListener).toBeCalledWith('open', expect.any(Function)); }); - - it(`message 이벤트: setRemoteProfileImageState(ev) 호출, 상대의 프로필사진을 설정함`, () => { - const setRemoteProfileImage = vi.fn(); - const arrayBuffer = new ArrayBuffer(8); - const type = 'image/png'; - const ev = { data: JSON.stringify({ type, arrayBuffer: new Uint8Array(arrayBuffer) }) } as any as MessageEvent; - - setRemoteProfileImageState({ ev, setRemoteProfileImage } as any); - - expect(setRemoteProfileImage).toBeCalledWith({ arrayBuffer, type }); - }); - - it(`on 이벤트: sendMyProfileImage(myProfile) 호출, 내 프로필 사진을 전송함`, () => { - const myProfile = { arrayBuffer: new ArrayBuffer(8), type: 'image/png' }; - const RTCDataChannelSendFn = vi.fn(); - - sendMyProfileImage.call({ send: RTCDataChannelSendFn } as any, { myProfile }); - - expect(RTCDataChannelSendFn).toBeCalledWith(JSON.stringify({ myProfile })); - }); }); describe('D. initNicknameChannel 함수 테스트', () => { it('nicknameChannel 데이터 채널 추가 + message와 open 이벤트가 등록됨.', () => { - currentRenderResult.initDataChannels(); + const { initDataChannels } = rerenderHook(); + initDataChannels(); expect(mockWebRTCModule.addDataChannel).toBeCalledWith('nicknameChannel'); expect(addEventListener).toBeCalledWith('message', expect.any(Function)); expect(addEventListener).toBeCalledWith('open', expect.any(Function)); }); - - it(`message 이벤트: setRemoteNicknameState(ev) 호출, 상대의 닉네임을 설정함`, () => { - const setRemoteNickname = vi.fn(); - const ev = { data: 'testNickName' } as any as MessageEvent; - - setRemoteNicknameState({ ev, setRemoteNickname } as any); - - expect(setRemoteNickname).toBeCalledWith('testNickName'); - }); - - it(`on 이벤트: sendMyNickname(myNickname) 호출, 내 닉네임을 전송함`, () => { - const myNickname = 'testNickName'; - const RTCDataChannelSendFn = vi.fn(); - - sendMyNickname.call({ send: RTCDataChannelSendFn } as any, { myNickname }); - - expect(RTCDataChannelSendFn).toBeCalledWith(myNickname); - }); }); }); it('dataChannels: webRTC.getDataChannels를 호출 후 그 결과를 반환함', () => { - expect(currentRenderResult.dataChannels).toBe(mockWebRTCModule.getDataChannels()); + const { dataChannels, initDataChannels } = rerenderHook(); + initDataChannels(); + + expect(dataChannels).toBe(mockWebRTCModule.getDataChannels()); }); }); diff --git a/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts b/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts new file mode 100644 index 00000000..b423e763 --- /dev/null +++ b/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts @@ -0,0 +1,212 @@ +import { act, renderHook } from '@testing-library/react'; + +import WebRTC from '@business/services/WebRTC'; + +import { useMediaInfo } from '@stores/zustandStores/useMediaInfo'; +import { useProfileInfo } from '@stores/zustandStores/useProfileInfo'; + +import { useDataChannelEventListener } from './useDataChannelEventListener'; + +vi.mock('@business/services/WebRTC'); + +describe('useDataChannelEventListener 훅 테스트', () => { + let mockWebRTCModule = WebRTC.getInstance(); + + function rerenderHook() { + const { + result: { + current: { + sendMyNickname, + sendMyProfileImage, + sendNowMediaStates, + setMediaStates, + setRemoteNicknameState, + setRemoteProfileImageState, + }, + }, + rerender, + } = renderHook(() => useDataChannelEventListener()); + + return { + rerender, + sendMyNickname, + sendMyProfileImage, + sendNowMediaStates, + setMediaStates, + setRemoteNicknameState, + setRemoteProfileImageState, + }; + } + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe(`setMediaStates 함수 테스트`, () => { + [ + { + scenario: 'type === video라면 onOrOff와 함께 setRemoteVideoOn 호출 (상대 비디오 상태 설정)', + data: JSON.stringify([ + { type: 'video', onOrOff: true }, + { type: 'video', onOrOff: false }, + ]), + }, + { + scenario: 'type === audio라면 onOrOff와 함께 setMicOn 호출 (상대 마이크 상태 설정)', + data: JSON.stringify([ + { type: 'audio', onOrOff: true }, + { type: 'audio', onOrOff: false }, + ]), + }, + ].forEach(({ scenario, data }) => { + it(scenario, () => { + const spySetRemoteMicOn = vi.spyOn(useMediaInfo.getState(), 'setRemoteMicOn'); + const spySetRemoteVideoOn = vi.spyOn(useMediaInfo.getState(), 'setRemoteVideoOn'); + const { setMediaStates } = rerenderHook(); + const parsedData = JSON.parse(data); + + act(() => { + setMediaStates({ ev: { data } } as any); + }); + parsedData.forEach(({ type, onOrOff }: { type: string; onOrOff: boolean }) => { + if (type === 'audio') { + expect(spySetRemoteMicOn).toBeCalledWith(onOrOff); + } else if (type === 'video') { + expect(spySetRemoteVideoOn).toBeCalledWith(onOrOff); + } + }); + }); + }); + }); + + describe('setRemoteProfileImageState 함수 테스트', () => { + it('인수로 들어온 파일을 상대의 프로필 사진으로 설정함', () => { + const spySetRemoteProfileImage = vi.spyOn(useProfileInfo.getState(), 'setRemoteProfile'); + const arrayBuffer = new ArrayBuffer(8); + const type = 'image/png'; + const ev = { data: JSON.stringify({ type, arrayBuffer: new Uint8Array(arrayBuffer) }) } as any as MessageEvent; + + const { setRemoteProfileImageState } = rerenderHook(); + + act(() => { + setRemoteProfileImageState({ ev }); + }); + + expect(spySetRemoteProfileImage).toBeCalledWith({ arrayBuffer, type }); + }); + }); + + describe('setRemoteNicknameState 함수 테스트', () => { + it('인수로 들어온 닉네임을 상대의 닉네임으로 설정함', () => { + const spySetRemoteNickname = vi.spyOn(useProfileInfo.getState(), 'setRemoteNickname'); + const ev = { data: 'testNickName' } as any as MessageEvent; + + const { setRemoteNicknameState } = rerenderHook(); + + act(() => { + setRemoteNicknameState({ ev }); + }); + + expect(spySetRemoteNickname).toBeCalledWith('testNickName'); + }); + }); + + describe('sendNowMediaStates 함수 테스트', () => { + [ + { + scenario: '현재 비디오 트랙이 꺼져있다면, 비디오 트랙을 전송하지 않음', + audioTrack: { enabled: true }, + videoTrack: { enabled: false }, + }, + { + scenario: '현재 오디오 트랙이 꺼져있다면, 오디오 트랙을 전송하지 않음', + audioTrack: { enabled: false }, + videoTrack: { enabled: true }, + }, + { + scenario: '현재 오디오 트랙, 비디오 트랙이 모두 켜져있다면, 오디오 트랙, 비디오 트랙을 전송함', + audioTrack: { enabled: true }, + videoTrack: { enabled: true }, + }, + ].forEach(({ scenario, audioTrack, videoTrack }) => { + it(scenario, () => { + vi.spyOn(mockWebRTCModule, 'getFirstAudioTrack').mockReturnValueOnce(audioTrack as any); + vi.spyOn(mockWebRTCModule, 'getFirstVideoTrack').mockReturnValueOnce(videoTrack as any); + const RTCDataChannelSendFn = vi.fn(); + + const { sendNowMediaStates } = rerenderHook(); + + act(() => { + sendNowMediaStates.call({ send: RTCDataChannelSendFn } as any); + }); + + expect(RTCDataChannelSendFn).toBeCalledWith( + JSON.stringify([ + { type: 'audio', onOrOff: audioTrack?.enabled }, + { type: 'video', onOrOff: videoTrack?.enabled }, + ]), + ); + }); + }); + }); + + describe('sendMyProfileImage 함수 테스트', () => { + [ + { + scenario: '내 프로필 사진이 존재한다면, 내 프로필 사진을 전송함', + myProfile: { arrayBuffer: new ArrayBuffer(8), type: 'image/png' }, + }, + { + scenario: '내 프로필 사진이 존재하지 않는다면, 내 프로필 사진을 전송하지 않음', + myProfile: undefined, + }, + ].forEach(({ scenario, myProfile }) => { + it(scenario, () => { + vi.spyOn(useProfileInfo.getState(), 'myProfile', 'get').mockReturnValueOnce(myProfile); + const RTCDataChannelSendFn = vi.fn(); + + const { sendMyProfileImage } = rerenderHook(); + + act(() => { + sendMyProfileImage.call({ send: RTCDataChannelSendFn } as any); + }); + + if (myProfile) { + expect(RTCDataChannelSendFn).toBeCalledWith(JSON.stringify({ myProfile })); + } else { + expect(RTCDataChannelSendFn).not.toBeCalled(); + } + }); + }); + }); + + describe('sendMyNickname 함수 테스트', () => { + [ + { + scenario: '내 닉네임이 존재한다면, 내 닉네임을 전송함', + myNickname: 'testNickName', + }, + { + scenario: '내 닉네임이 존재하지 않는다면, 내 닉네임을 전송하지 않음', + myNickname: undefined, + }, + ].forEach(({ scenario, myNickname }) => { + it(scenario, () => { + vi.spyOn(useProfileInfo.getState(), 'myNickname', 'get').mockReturnValueOnce(myNickname); + const RTCDataChannelSendFn = vi.fn(); + + const { sendMyNickname } = rerenderHook(); + + act(() => { + sendMyNickname.call({ send: RTCDataChannelSendFn } as any); + }); + + if (myNickname) { + expect(RTCDataChannelSendFn).toBeCalledWith(myNickname); + } else { + expect(RTCDataChannelSendFn).not.toBeCalled(); + } + }); + }); + }); +}); diff --git a/frontend/src/stores/zustandStores/useProfileInfo.ts b/frontend/src/stores/zustandStores/useProfileInfo.ts index 22f02db8..c316742e 100644 --- a/frontend/src/stores/zustandStores/useProfileInfo.ts +++ b/frontend/src/stores/zustandStores/useProfileInfo.ts @@ -8,7 +8,7 @@ export interface ProfileInfo { interface ProfileInfoState { myNickname?: string; - myProfile?: ProfileInfo; + myProfile?: ProfileInfo | undefined; remoteNickname?: string; remoteProfile?: ProfileInfo; } @@ -22,6 +22,10 @@ interface ProfileInfoActions { export const useProfileInfo = create()( devtools(set => ({ + myNickname: '', + myProfile: undefined, + remoteNickname: '', + remoteProfile: undefined, setMyNickname: (myNickname: string) => set(() => ({ myNickname })), setMyProfile: (myProfile: ProfileInfo) => set(() => ({ myProfile })), setRemoteNickname: (remoteNickname: string) => set(() => ({ remoteNickname })), From 214e37eded7cc6b8bbeca138bae9591455967c00 Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 17:40:56 +0900 Subject: [PATCH 6/7] =?UTF-8?q?test:=20useDataChannelEventListener=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 전에 전역상태 수정하고서 테스트해봄 --- .../hooks/useWebRTC/useDataChannelEventListener.spec.ts | 4 +++- frontend/src/stores/zustandStores/useProfileInfo.ts | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts b/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts index b423e763..38a63968 100644 --- a/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts +++ b/frontend/src/business/hooks/useWebRTC/useDataChannelEventListener.spec.ts @@ -162,7 +162,8 @@ describe('useDataChannelEventListener 훅 테스트', () => { }, ].forEach(({ scenario, myProfile }) => { it(scenario, () => { - vi.spyOn(useProfileInfo.getState(), 'myProfile', 'get').mockReturnValueOnce(myProfile); + useProfileInfo.getState().setMyProfile(myProfile as any); + vi.spyOn(useProfileInfo.getState(), 'myProfile', 'get')?.mockReturnValueOnce(myProfile); const RTCDataChannelSendFn = vi.fn(); const { sendMyProfileImage } = rerenderHook(); @@ -192,6 +193,7 @@ describe('useDataChannelEventListener 훅 테스트', () => { }, ].forEach(({ scenario, myNickname }) => { it(scenario, () => { + useProfileInfo.getState().setMyNickname(myNickname as any); vi.spyOn(useProfileInfo.getState(), 'myNickname', 'get').mockReturnValueOnce(myNickname); const RTCDataChannelSendFn = vi.fn(); diff --git a/frontend/src/stores/zustandStores/useProfileInfo.ts b/frontend/src/stores/zustandStores/useProfileInfo.ts index c316742e..22f02db8 100644 --- a/frontend/src/stores/zustandStores/useProfileInfo.ts +++ b/frontend/src/stores/zustandStores/useProfileInfo.ts @@ -8,7 +8,7 @@ export interface ProfileInfo { interface ProfileInfoState { myNickname?: string; - myProfile?: ProfileInfo | undefined; + myProfile?: ProfileInfo; remoteNickname?: string; remoteProfile?: ProfileInfo; } @@ -22,10 +22,6 @@ interface ProfileInfoActions { export const useProfileInfo = create()( devtools(set => ({ - myNickname: '', - myProfile: undefined, - remoteNickname: '', - remoteProfile: undefined, setMyNickname: (myNickname: string) => set(() => ({ myNickname })), setMyProfile: (myProfile: ProfileInfo) => set(() => ({ myProfile })), setRemoteNickname: (remoteNickname: string) => set(() => ({ remoteNickname })), From e6ff7a903ed4ab277431937d02bd9bb802186763 Mon Sep 17 00:00:00 2001 From: Song_Minhyung Date: Wed, 31 Jan 2024 20:20:35 +0900 Subject: [PATCH 7/7] =?UTF-8?q?conflict:=20=EB=B3=91=ED=95=A9=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/business/hooks/useWebRTC/useSignalingSocket.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/business/hooks/useWebRTC/useSignalingSocket.spec.ts b/frontend/src/business/hooks/useWebRTC/useSignalingSocket.spec.ts index b651cb17..19aeefda 100644 --- a/frontend/src/business/hooks/useWebRTC/useSignalingSocket.spec.ts +++ b/frontend/src/business/hooks/useWebRTC/useSignalingSocket.spec.ts @@ -62,7 +62,7 @@ describe('useSignalingSocket 훅', () => { const testDatas = [{ onSuccess: vi.fn() }, { onHostExit: vi.fn() }, { onFail: vi.fn() }, { onFull: vi.fn() }]; testDatas.forEach(({ onFail, onFull, onHostExit, onSuccess }) => { - initGuestSocketEvents({ password, roomName, closeOverlay: vi.fn(), onSuccess, onHostExit, onFail, onFull }); + initGuestSocketEvents({ password, roomName, closePopup: vi.fn(), onSuccess, onHostExit, onFail, onFull }); expect(socket.emit).toBeCalledWith('joinRoom', roomName, password);