Skip to content

Commit 036b08f

Browse files
committed
test: 변경된 로직에 맞게 use-topic-events 테스트 코드 추가
1 parent 5682f13 commit 036b08f

1 file changed

Lines changed: 94 additions & 88 deletions

File tree

Lines changed: 94 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
/**
22
* @jest-environment jsdom
33
*/
4+
import { useSession } from 'next-auth/react';
5+
import { useRouter } from 'next/navigation';
46
import { useQueryClient } from '@tanstack/react-query';
57
import { act, renderHook } from '@testing-library/react';
68
import toast from 'react-hot-toast';
79
import { SSE_EVENT_TYPES } from '@/constants/sse-events';
810
import { useTopicEvents } from '@/hooks/topic/use-topic-events';
911

10-
// useQueryClient 모킹
12+
// 1. 외부 모듈 모킹
1113
jest.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+
}));
1422
jest.mock('react-hot-toast');
1523

1624
describe('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

Comments
 (0)