1
+ // useSse.test.ts
1
2
import { act , renderHook } from '@testing-library/react' ;
2
- import { afterEach , describe , expect , test , vi } from 'vitest' ;
3
+ import { afterEach , beforeEach , describe , expect , test , vi } from 'vitest' ;
3
4
import useSse , { ReadyState } from '../index' ;
4
5
5
6
class MockEventSource {
@@ -10,6 +11,7 @@ class MockEventSource {
10
11
onmessage : ( ( this : EventSource , ev : MessageEvent ) => any ) | null = null ;
11
12
onerror : ( ( this : EventSource , ev : Event ) => any ) | null = null ;
12
13
private listeners : Record < string , Array < ( ev : Event ) => void > > = { } ;
14
+ private openTimeout ?: NodeJS . Timeout ;
13
15
14
16
static CONNECTING = 0 ;
15
17
static OPEN = 1 ;
@@ -19,9 +21,10 @@ class MockEventSource {
19
21
this . url = url ;
20
22
this . withCredentials = Boolean ( init ?. withCredentials ) ;
21
23
this . readyState = MockEventSource . CONNECTING ;
22
- setTimeout ( ( ) => {
24
+
25
+ this . openTimeout = setTimeout ( ( ) => {
23
26
this . readyState = MockEventSource . OPEN ;
24
- this . onopen && this . onopen ( new Event ( 'open' ) ) ;
27
+ this . onopen ?. ( new Event ( 'open' ) ) ;
25
28
} , 10 ) ;
26
29
}
27
30
@@ -35,69 +38,153 @@ class MockEventSource {
35
38
}
36
39
37
40
emitMessage ( data : any ) {
38
- this . onmessage && this . onmessage ( new MessageEvent ( 'message' , { data } ) ) ;
41
+ if ( this . readyState !== MockEventSource . OPEN ) return ;
42
+ this . onmessage ?.( new MessageEvent ( 'message' , { data } ) ) ;
39
43
}
40
44
41
45
emitError ( ) {
42
- this . onerror && this . onerror ( new Event ( 'error' ) ) ;
46
+ this . onerror ?.( new Event ( 'error' ) ) ;
47
+ }
48
+
49
+ emitRetry ( ms : number ) {
50
+ const ev = new MessageEvent ( 'message' , { data : '' } ) ;
51
+ ( ev as any ) . retry = ms ;
52
+ this . onmessage ?.( ev ) ;
43
53
}
44
54
45
55
close ( ) {
46
56
this . readyState = MockEventSource . CLOSED ;
57
+ if ( this . openTimeout ) clearTimeout ( this . openTimeout ) ;
47
58
}
48
59
}
49
60
50
- describe ( 'useSse' , ( ) => {
61
+ describe ( 'useSse Hook ' , ( ) => {
51
62
const OriginalEventSource = ( globalThis as any ) . EventSource ;
52
63
64
+ beforeEach ( ( ) => {
65
+ vi . useFakeTimers ( ) ;
66
+ ( globalThis as any ) . EventSource = MockEventSource ;
67
+ } ) ;
68
+
53
69
afterEach ( ( ) => {
70
+ vi . runAllTimers ( ) ;
71
+ vi . useRealTimers ( ) ;
54
72
( globalThis as any ) . EventSource = OriginalEventSource ;
73
+ vi . restoreAllMocks ( ) ;
55
74
} ) ;
56
75
57
- test ( 'should connect and receive message' , async ( ) => {
58
- ( globalThis as any ) . EventSource = MockEventSource as any ;
76
+ test ( 'should connect and receive message' , ( ) => {
77
+ const hook = renderHook ( ( ) => useSse ( '/sse' ) ) ;
78
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Connecting ) ;
79
+
80
+ act ( ( ) => vi . advanceTimersByTime ( 20 ) ) ;
81
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Open ) ;
82
+
83
+ act ( ( ) => {
84
+ const es = hook . result . current . eventSource as unknown as MockEventSource ;
85
+ es . emitMessage ( 'hello' ) ;
86
+ } ) ;
87
+ expect ( hook . result . current . latestMessage ?. data ) . toBe ( 'hello' ) ;
59
88
60
- const hooks = renderHook ( ( ) => useSse ( '/sse' ) ) ;
89
+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
90
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Closed ) ;
91
+ } ) ;
61
92
62
- // not manual: should start connecting immediately
63
- expect ( hooks . result . current . readyState ) . toBe ( ReadyState . Connecting ) ;
93
+ test ( 'manual mode should not auto connect' , ( ) => {
94
+ const hook = renderHook ( ( ) => useSse ( '/sse' , { manual : true } ) ) ;
95
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Closed ) ;
64
96
65
- await act ( async ( ) => {
66
- await new Promise ( ( r ) => setTimeout ( r , 20 ) ) ;
97
+ act ( ( ) => {
98
+ hook . result . current . connect ( ) ;
99
+ vi . advanceTimersByTime ( 20 ) ;
67
100
} ) ;
101
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Open ) ;
68
102
69
- expect ( hooks . result . current . readyState ) . toBe ( ReadyState . Open ) ;
103
+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
104
+ } ) ;
105
+
106
+ test ( 'should handle custom events' , ( ) => {
107
+ const onEvent = vi . fn ( ) ;
108
+ const hook = renderHook ( ( ) => useSse ( '/sse' , { onEvent } ) ) ;
109
+ act ( ( ) => vi . advanceTimersByTime ( 20 ) ) ;
70
110
71
111
act ( ( ) => {
72
- const es = hooks . result . current . eventSource as unknown as MockEventSource ;
73
- es . emitMessage ( 'hello' ) ;
112
+ const es = hook . result . current . eventSource as unknown as MockEventSource ;
113
+ es . dispatchEvent ( 'custom' , new MessageEvent ( 'custom' , { data : 'foo' } ) ) ;
74
114
} ) ;
75
- expect ( hooks . result . current . latestMessage ?. data ) . toBe ( 'hello' ) ;
115
+
116
+ expect ( onEvent ) . toHaveBeenCalledWith (
117
+ 'custom' ,
118
+ expect . objectContaining ( { data : 'foo' } ) ,
119
+ expect . any ( MockEventSource ) ,
120
+ ) ;
121
+
122
+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
76
123
} ) ;
77
124
78
- test ( 'manual should not auto connect' , async ( ) => {
79
- ( globalThis as any ) . EventSource = MockEventSource as any ;
125
+ test ( 'should reconnect on error respecting reconnectLimit' , ( ) => {
126
+ const hook = renderHook ( ( ) => useSse ( '/sse' , { reconnectLimit : 1 , reconnectInterval : 5 } ) ) ;
127
+ act ( ( ) => vi . advanceTimersByTime ( 20 ) ) ;
128
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Open ) ;
129
+
130
+ act ( ( ) => {
131
+ const es = hook . result . current . eventSource as unknown as MockEventSource ;
132
+ es . emitError ( ) ;
133
+ vi . advanceTimersByTime ( 20 ) ;
134
+ } ) ;
135
+
136
+ expect (
137
+ [ ReadyState . Reconnecting , ReadyState . Open ] . includes ( hook . result . current . readyState ) ,
138
+ ) . toBe ( true ) ;
139
+
140
+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
141
+ } ) ;
80
142
81
- const hooks = renderHook ( ( ) => useSse ( '/sse' , { manual : true } ) ) ;
82
- expect ( hooks . result . current . readyState ) . toBe ( ReadyState . Closed ) ;
143
+ test ( 'should respect server retry when enabled' , ( ) => {
144
+ const hook = renderHook ( ( ) =>
145
+ useSse ( '/sse' , { reconnectLimit : 1 , reconnectInterval : 5 , respectServerRetry : true } ) ,
146
+ ) ;
147
+ act ( ( ) => vi . advanceTimersByTime ( 20 ) ) ;
148
+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Open ) ;
83
149
84
- await act ( async ( ) => {
85
- hooks . result . current . connect ( ) ;
86
- await new Promise ( ( r ) => setTimeout ( r , 20 ) ) ;
150
+ act ( ( ) => {
151
+ const es = hook . result . current . eventSource as unknown as MockEventSource ;
152
+ es . emitRetry ( 50 ) ;
153
+ es . emitError ( ) ;
154
+ vi . advanceTimersByTime ( 60 ) ;
87
155
} ) ;
88
156
89
- expect ( hooks . result . current . readyState ) . toBe ( ReadyState . Open ) ;
157
+ expect (
158
+ [ ReadyState . Reconnecting , ReadyState . Open ] . includes ( hook . result . current . readyState ) ,
159
+ ) . toBe ( true ) ;
160
+
161
+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
90
162
} ) ;
91
163
92
- test ( 'disconnect should close' , async ( ) => {
93
- ( globalThis as any ) . EventSource = MockEventSource as any ;
164
+ test ( 'should trigger all callbacks' , ( ) => {
165
+ const onOpen = vi . fn ( ) ;
166
+ const onMessage = vi . fn ( ) ;
167
+ const onError = vi . fn ( ) ;
168
+ const onReconnect = vi . fn ( ) ;
169
+
170
+ const hook = renderHook ( ( ) => useSse ( '/sse' , { onOpen, onMessage, onError, onReconnect } ) ) ;
171
+ act ( ( ) => vi . advanceTimersByTime ( 20 ) ) ;
172
+ expect ( onOpen ) . toHaveBeenCalled ( ) ;
94
173
95
- const hooks = renderHook ( ( ) => useSse ( '/sse' ) ) ;
96
- await act ( async ( ) => {
97
- await new Promise ( ( r ) => setTimeout ( r , 20 ) ) ;
174
+ act ( ( ) => {
175
+ const es = hook . result . current . eventSource as unknown as MockEventSource ;
176
+ es . emitMessage ( 'world' ) ;
98
177
} ) ;
99
- expect ( hooks . result . current . readyState ) . toBe ( ReadyState . Open ) ;
100
- act ( ( ) => hooks . result . current . disconnect ( ) ) ;
101
- expect ( hooks . result . current . readyState ) . toBe ( ReadyState . Closed ) ;
178
+ expect ( onMessage ) . toHaveBeenCalled ( ) ;
179
+
180
+ act ( ( ) => {
181
+ const es = hook . result . current . eventSource as unknown as MockEventSource ;
182
+ es . emitError ( ) ;
183
+ vi . advanceTimersByTime ( 20 ) ;
184
+ } ) ;
185
+ expect ( onError ) . toHaveBeenCalled ( ) ;
186
+ expect ( onReconnect ) . toHaveBeenCalled ( ) ;
187
+
188
+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
102
189
} ) ;
103
190
} ) ;
0 commit comments