1+ // useSse.test.ts
12import { 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' ;
34import useSse , { ReadyState } from '../index' ;
45
56class MockEventSource {
@@ -10,6 +11,7 @@ class MockEventSource {
1011 onmessage : ( ( this : EventSource , ev : MessageEvent ) => any ) | null = null ;
1112 onerror : ( ( this : EventSource , ev : Event ) => any ) | null = null ;
1213 private listeners : Record < string , Array < ( ev : Event ) => void > > = { } ;
14+ private openTimeout ?: NodeJS . Timeout ;
1315
1416 static CONNECTING = 0 ;
1517 static OPEN = 1 ;
@@ -19,9 +21,10 @@ class MockEventSource {
1921 this . url = url ;
2022 this . withCredentials = Boolean ( init ?. withCredentials ) ;
2123 this . readyState = MockEventSource . CONNECTING ;
22- setTimeout ( ( ) => {
24+
25+ this . openTimeout = setTimeout ( ( ) => {
2326 this . readyState = MockEventSource . OPEN ;
24- this . onopen && this . onopen ( new Event ( 'open' ) ) ;
27+ this . onopen ?. ( new Event ( 'open' ) ) ;
2528 } , 10 ) ;
2629 }
2730
@@ -35,69 +38,153 @@ class MockEventSource {
3538 }
3639
3740 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 } ) ) ;
3943 }
4044
4145 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 ) ;
4353 }
4454
4555 close ( ) {
4656 this . readyState = MockEventSource . CLOSED ;
57+ if ( this . openTimeout ) clearTimeout ( this . openTimeout ) ;
4758 }
4859}
4960
50- describe ( 'useSse' , ( ) => {
61+ describe ( 'useSse Hook ' , ( ) => {
5162 const OriginalEventSource = ( globalThis as any ) . EventSource ;
5263
64+ beforeEach ( ( ) => {
65+ vi . useFakeTimers ( ) ;
66+ ( globalThis as any ) . EventSource = MockEventSource ;
67+ } ) ;
68+
5369 afterEach ( ( ) => {
70+ vi . runAllTimers ( ) ;
71+ vi . useRealTimers ( ) ;
5472 ( globalThis as any ) . EventSource = OriginalEventSource ;
73+ vi . restoreAllMocks ( ) ;
5574 } ) ;
5675
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' ) ;
5988
60- const hooks = renderHook ( ( ) => useSse ( '/sse' ) ) ;
89+ act ( ( ) => hook . result . current . disconnect ( ) ) ;
90+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Closed ) ;
91+ } ) ;
6192
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 ) ;
6496
65- await act ( async ( ) => {
66- await new Promise ( ( r ) => setTimeout ( r , 20 ) ) ;
97+ act ( ( ) => {
98+ hook . result . current . connect ( ) ;
99+ vi . advanceTimersByTime ( 20 ) ;
67100 } ) ;
101+ expect ( hook . result . current . readyState ) . toBe ( ReadyState . Open ) ;
68102
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 ) ) ;
70110
71111 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' } ) ) ;
74114 } ) ;
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 ( ) ) ;
76123 } ) ;
77124
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+ } ) ;
80142
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 ) ;
83149
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 ) ;
87155 } ) ;
88156
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 ( ) ) ;
90162 } ) ;
91163
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 ( ) ;
94173
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' ) ;
98177 } ) ;
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 ( ) ) ;
102189 } ) ;
103190} ) ;
0 commit comments