11/**
22 * @jest -environment jsdom
33 */
4+ import { useSession } from 'next-auth/react' ;
5+ import { useRouter } from 'next/navigation' ;
46import { useQueryClient } from '@tanstack/react-query' ;
57import { act , renderHook } from '@testing-library/react' ;
68import toast from 'react-hot-toast' ;
79import { SSE_EVENT_TYPES } from '@/constants/sse-events' ;
810import { useTopicEvents } from '@/hooks/topic/use-topic-events' ;
911
10- // useQueryClient 모킹
12+ // 1. 외부 모듈 모킹
1113jest . mock ( '@tanstack/react-query' , ( ) => ( {
1214 useQueryClient : jest . fn ( ) ,
1315} ) ) ;
16+ jest . mock ( 'next-auth/react' , ( ) => ( {
17+ useSession : jest . fn ( ) ,
18+ } ) ) ;
19+ jest . mock ( 'next/navigation' , ( ) => ( {
20+ useRouter : jest . fn ( ) ,
21+ } ) ) ;
1422jest . mock ( 'react-hot-toast' ) ;
1523
1624describe ( 'useTopicEvents' , ( ) => {
1725 let mockEventSource : any ;
1826 let mockInvalidateQueries : jest . Mock ;
27+ let mockRouter : any ;
28+ const mockUserId = 'user-123' ;
29+ const topicId = 'test-topic-id' ;
1930
2031 beforeEach ( ( ) => {
2132 // 1. QueryClient 모킹
@@ -24,7 +35,18 @@ describe('useTopicEvents', () => {
2435 invalidateQueries : mockInvalidateQueries ,
2536 } ) ;
2637
27- // 2. EventSource 인스턴스 모킹
38+ // 2. Session 모킹
39+ ( useSession as jest . Mock ) . mockReturnValue ( {
40+ data : { user : { id : mockUserId } } ,
41+ } ) ;
42+
43+ // 3. Router 모킹
44+ mockRouter = {
45+ refresh : jest . fn ( ) ,
46+ } ;
47+ ( useRouter as jest . Mock ) . mockReturnValue ( mockRouter ) ;
48+
49+ // 4. EventSource 인스턴스 모킹
2850 mockEventSource = {
2951 onopen : null ,
3052 onmessage : null ,
@@ -34,152 +56,136 @@ describe('useTopicEvents', () => {
3456 removeEventListener : jest . fn ( ) ,
3557 } ;
3658
37- // 3 . EventSource 생성자 모킹
38- const mockEventSourceConstructor = jest . fn ( ( ) => mockEventSource ) ;
59+ // 5 . EventSource 생성자 모킹
60+ global . EventSource = jest . fn ( ( ) => mockEventSource ) as any ;
3961
40- global . EventSource = mockEventSourceConstructor as unknown as typeof EventSource ;
62+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
4163 } ) ;
4264
4365 afterEach ( ( ) => {
4466 jest . clearAllMocks ( ) ;
4567 } ) ;
4668
4769 it ( '연결 성공 시 isConnected가 true로 변경되어야 한다' , ( ) => {
48- const { result } = renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
49-
50- // 초기값 확인
51- expect ( result . current . isConnected ) . toBe ( false ) ;
52-
53- // onopen 이벤트 발생
70+ const { result } = renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
5471 act ( ( ) => {
5572 mockEventSource . onopen ( ) ;
5673 } ) ;
57-
5874 expect ( result . current . isConnected ) . toBe ( true ) ;
5975 } ) ;
6076
6177 it ( '에러 발생 시 isConnected가 false로 변경되어야 한다' , ( ) => {
62- const { result } = renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
63-
64- // 먼저 연결 상태로 만듦
78+ const { result } = renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
6579 act ( ( ) => {
66- mockEventSource . onopen ( ) ;
80+ mockEventSource . onerror ( new Event ( 'error' ) ) ;
6781 } ) ;
68- expect ( result . current . isConnected ) . toBe ( true ) ;
69-
70- // 에러 발생
71- act ( ( ) => {
72- mockEventSource . onerror ( ) ;
73- } ) ;
74-
7582 expect ( result . current . isConnected ) . toBe ( false ) ;
7683 } ) ;
7784
7885 it ( 'ISSUE_STATUS_CHANGED 이벤트 수신 시 쿼리를 무효화해야 한다' , ( ) => {
79- renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
80-
81- // addEventListener 호출 확인
82- expect ( mockEventSource . addEventListener ) . toHaveBeenCalledWith (
83- SSE_EVENT_TYPES . ISSUE_STATUS_CHANGED ,
84- expect . any ( Function ) ,
85- ) ;
86+ renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
8687
87- // 등록된 이벤트 핸들러 가져오기
8888 const handler = mockEventSource . addEventListener . mock . calls . find (
8989 ( call : any ) => call [ 0 ] === SSE_EVENT_TYPES . ISSUE_STATUS_CHANGED ,
9090 ) [ 1 ] ;
9191
92- // 가짜 이벤트 데이터
93- const mockEvent = {
94- data : JSON . stringify ( {
95- status : 'OPEN' ,
96- issueId : 'test-issue-id' ,
97- topicId : 'test-topic-id' ,
98- } ) ,
99- } ;
100-
101- // 핸들러 실행
92+ const mockEvent = { data : JSON . stringify ( { issueId : 'issue-1' } ) } ;
10293 act ( ( ) => {
10394 handler ( mockEvent ) ;
10495 } ) ;
10596
106- // 사이드바 이슈 목록 갱신 확인
10797 expect ( mockInvalidateQueries ) . toHaveBeenCalledWith ( {
108- queryKey : [ 'topics' , 'test-topic-id' , 'issues' ] ,
98+ queryKey : [ 'topics' , topicId , 'issues' ] ,
10999 } ) ;
110-
111- // 특정 이슈 데이터 갱신 확인
112100 expect ( mockInvalidateQueries ) . toHaveBeenCalledWith ( {
113- queryKey : [ 'issues' , 'test- issue-id ' ] ,
101+ queryKey : [ 'issues' , 'issue-1 ' ] ,
114102 } ) ;
115103 } ) ;
116104
117- it ( '마운트 시 beforeunload 리스너를 등록해야 한다' , ( ) => {
118- const addSpy = jest . spyOn ( window , 'addEventListener' ) ;
105+ describe ( 'ISSUE_DELETED 이벤트' , ( ) => {
106+ it ( '본인이 삭제한 이벤트(actorId === userId)인 경우 아무것도 하지 않아야 한다' , ( ) => {
107+ renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
119108
120- renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
109+ const handler = mockEventSource . addEventListener . mock . calls . find (
110+ ( call : any ) => call [ 0 ] === SSE_EVENT_TYPES . ISSUE_DELETED ,
111+ ) [ 1 ] ;
121112
122- expect ( addSpy ) . toHaveBeenCalledWith ( 'beforeunload' , expect . any ( Function ) ) ;
113+ act ( ( ) => {
114+ handler ( { data : JSON . stringify ( { actorId : mockUserId , issueId : 'i1' } ) } ) ;
115+ } ) ;
123116
124- addSpy . mockRestore ( ) ;
125- } ) ;
117+ expect ( mockInvalidateQueries ) . not . toHaveBeenCalled ( ) ;
118+ expect ( toast . error ) . not . toHaveBeenCalled ( ) ;
119+ expect ( mockRouter . refresh ) . not . toHaveBeenCalled ( ) ;
120+ } ) ;
126121
127- it ( 'beforeunload 발생 시 EventSource를 닫아야 한다' , ( ) => {
128- const addSpy = jest . spyOn ( window , 'addEventListener' ) ;
122+ it ( '타인이 삭제한 경우 쿼리를 무효화하고 토스트를 띄우며 페이지를 리프레시해야 한다' , ( ) => {
123+ renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
129124
130- renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
125+ const handler = mockEventSource . addEventListener . mock . calls . find (
126+ ( call : any ) => call [ 0 ] === SSE_EVENT_TYPES . ISSUE_DELETED ,
127+ ) [ 1 ] ;
131128
132- const handler = addSpy . mock . calls . find (
133- ( call ) => call [ 0 ] === 'beforeunload' ,
134- ) ! [ 1 ] as EventListener ;
129+ act ( ( ) => {
130+ handler ( { data : JSON . stringify ( { actorId : 'other-user' , issueId : 'issue-99' } ) } ) ;
131+ } ) ;
135132
136- act ( ( ) => {
137- handler ( new Event ( 'beforeunload' ) ) ;
133+ // 1. 쿼리 무효화 확인
134+ expect ( mockInvalidateQueries ) . toHaveBeenCalledWith ( { queryKey : [ 'topics' , topicId ] } ) ;
135+ expect ( mockInvalidateQueries ) . toHaveBeenCalledWith ( { queryKey : [ 'issues' , 'issue-99' ] } ) ;
136+
137+ // 2. 피드백 확인
138+ expect ( toast . error ) . toHaveBeenCalledWith ( '이슈가 삭제되었습니다.' ) ;
139+
140+ // 3. 페이지 리프레시 확인
141+ expect ( mockRouter . refresh ) . toHaveBeenCalled ( ) ;
138142 } ) ;
139143
140- expect ( mockEventSource . close ) . toHaveBeenCalled ( ) ;
144+ it ( 'issueId가 없는 삭제 이벤트의 경우 이슈 상세 쿼리 무효화는 건너뛰어야 한다' , ( ) => {
145+ renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
146+
147+ const handler = mockEventSource . addEventListener . mock . calls . find (
148+ ( call : any ) => call [ 0 ] === SSE_EVENT_TYPES . ISSUE_DELETED ,
149+ ) [ 1 ] ;
141150
142- addSpy . mockRestore ( ) ;
151+ act ( ( ) => {
152+ handler ( { data : JSON . stringify ( { actorId : 'other-user' , issueId : null } ) } ) ;
153+ } ) ;
154+
155+ expect ( mockInvalidateQueries ) . toHaveBeenCalledWith ( { queryKey : [ 'topics' , topicId ] } ) ;
156+ expect ( mockInvalidateQueries ) . not . toHaveBeenCalledWith ( { queryKey : [ 'issues' , null ] } ) ;
157+ } ) ;
143158 } ) ;
144159
145- it ( '언마운트 시 EventSource와 beforeunload 리스너를 정리해야 한다' , ( ) => {
146- const removeSpy = jest . spyOn ( window , 'removeEventListener' ) ;
160+ it ( 'beforeunload 발생 시 EventSource를 닫아야 한다' , ( ) => {
161+ const addSpy = jest . spyOn ( window , 'addEventListener' ) ;
162+ renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
163+ const handler = addSpy . mock . calls . find ( ( call ) => call [ 0 ] === 'beforeunload' ) ! [ 1 ] as any ;
147164
148- const { unmount } = renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
165+ act ( ( ) => {
166+ handler ( ) ;
167+ } ) ;
168+ expect ( mockEventSource . close ) . toHaveBeenCalled ( ) ;
169+ } ) ;
149170
171+ it ( '언마운트 시 리소스를 정리해야 한다' , ( ) => {
172+ const removeSpy = jest . spyOn ( window , 'removeEventListener' ) ;
173+ const { unmount } = renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
150174 unmount ( ) ;
151-
152175 expect ( mockEventSource . close ) . toHaveBeenCalled ( ) ;
153176 expect ( removeSpy ) . toHaveBeenCalledWith ( 'beforeunload' , expect . any ( Function ) ) ;
154-
155- removeSpy . mockRestore ( ) ;
156177 } ) ;
157178
158- it ( '기본 메시지("connected") 수신 시 토스트를 띄워야 한다' , ( ) => {
159- // 1. 훅 렌더링
160- renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' } ) ) ;
161-
162- // 2. 가짜 메시지 이벤트 데이터 생성
163- const mockMessageEvent = {
164- data : JSON . stringify ( { type : 'connected' } ) ,
165- } ;
166-
167- // 3. onmessage 핸들러 실행
179+ it ( '기본 connected 메시지 수신 시 성공 토스트를 띄워야 한다' , ( ) => {
180+ renderHook ( ( ) => useTopicEvents ( { topicId } ) ) ;
168181 act ( ( ) => {
169- mockEventSource . onmessage ( mockMessageEvent ) ;
182+ mockEventSource . onmessage ( { data : JSON . stringify ( { type : 'connected' } ) } ) ;
170183 } ) ;
171-
172- // 4. 검증: toast.success가 호출되었는지 확인
173184 expect ( toast . success ) . toHaveBeenCalledWith ( '토픽에 연결되었습니다' ) ;
174185 } ) ;
175186
176- it ( 'enabled가 false이면 EventSource를 연결하지 않아야 한다' , ( ) => {
177- // EventSource 생성자 호출 기록 초기화
178- ( global . EventSource as unknown as jest . Mock ) . mockClear ( ) ;
179-
180- renderHook ( ( ) => useTopicEvents ( { topicId : 'test-topic-id' , enabled : false } ) ) ;
181-
182- // enabled: false이므로 생성자가 호출되지 않아야 함
187+ it ( 'enabled가 false이면 연결을 시도하지 않는다' , ( ) => {
188+ renderHook ( ( ) => useTopicEvents ( { topicId, enabled : false } ) ) ;
183189 expect ( global . EventSource ) . not . toHaveBeenCalled ( ) ;
184190 } ) ;
185191} ) ;
0 commit comments