1+ import { AppState } from 'react-native' ;
2+ import { AppStateWebSocketManager } from './AppStateWebSocketManager' ;
3+ import { MobileBackendWebSocketService } from './websocket-service-init' ;
4+
5+ // Mock react-native AppState
6+ jest . mock ( 'react-native' , ( ) => ( {
7+ AppState : {
8+ currentState : 'active' ,
9+ addEventListener : jest . fn ( ) ,
10+ } ,
11+ } ) ) ;
12+
13+ // Mock Logger
14+ jest . mock ( '../../../../util/Logger' , ( ) => ( {
15+ error : jest . fn ( ) ,
16+ } ) ) ;
17+
18+ // Mock WebSocket service
19+ const mockWebSocketService = {
20+ connect : jest . fn ( ) ,
21+ disconnect : jest . fn ( ) ,
22+ sendMessage : jest . fn ( ) ,
23+ getSessionId : jest . fn ( ) ,
24+ } as unknown as MobileBackendWebSocketService ;
25+
26+ describe ( 'AppStateWebSocketManager' , ( ) => {
27+ let manager : AppStateWebSocketManager ;
28+ let mockAddEventListener : jest . MockedFunction < typeof AppState . addEventListener > ;
29+
30+ beforeEach ( ( ) => {
31+ jest . clearAllMocks ( ) ;
32+ mockAddEventListener = AppState . addEventListener as jest . MockedFunction < typeof AppState . addEventListener > ;
33+
34+ // Mock session ID
35+ ( mockWebSocketService . getSessionId as jest . Mock ) . mockReturnValue ( 'test-session-123' ) ;
36+
37+ // Create a new manager instance for each test
38+ manager = new AppStateWebSocketManager ( ) ;
39+ manager . setWebSocketService ( mockWebSocketService ) ;
40+ } ) ;
41+
42+ afterEach ( ( ) => {
43+ manager . cleanup ( ) ;
44+ } ) ;
45+
46+ describe ( 'initialization' , ( ) => {
47+ it ( 'should start app state listener when WebSocket service is set' , ( ) => {
48+ expect ( mockAddEventListener ) . toHaveBeenCalledWith ( 'change' , expect . any ( Function ) ) ;
49+ } ) ;
50+
51+ it ( 'should get current app state on initialization' , ( ) => {
52+ const status = manager . getConnectionStatus ( ) ;
53+ expect ( status . appState ) . toBe ( 'active' ) ;
54+ } ) ;
55+ } ) ;
56+
57+ describe ( 'connect method' , ( ) => {
58+ it ( 'should connect immediately when app is active' , async ( ) => {
59+ await manager . connect ( ) ;
60+
61+ expect ( mockWebSocketService . connect ) . toHaveBeenCalled ( ) ;
62+ const status = manager . getConnectionStatus ( ) ;
63+ expect ( status . isConnected ) . toBe ( true ) ;
64+ expect ( status . shouldAutoConnect ) . toBe ( true ) ;
65+ expect ( status . hasActiveSession ) . toBe ( true ) ;
66+ expect ( status . sessionId ) . toBe ( 'test-session-123' ) ;
67+ } ) ;
68+
69+ it ( 'should defer connection when app is not active' , async ( ) => {
70+ // Mock app being in background
71+ ( AppState as any ) . currentState = 'background' ;
72+ manager = new AppStateWebSocketManager ( ) ;
73+ manager . setWebSocketService ( mockWebSocketService ) ;
74+
75+ await manager . connect ( ) ;
76+
77+ expect ( mockWebSocketService . connect ) . not . toHaveBeenCalled ( ) ;
78+ const status = manager . getConnectionStatus ( ) ;
79+ expect ( status . isConnected ) . toBe ( false ) ;
80+ expect ( status . shouldAutoConnect ) . toBe ( true ) ;
81+ expect ( status . hasActiveSession ) . toBe ( false ) ;
82+ } ) ;
83+ } ) ;
84+
85+ describe ( 'app state changes with session preservation' , ( ) => {
86+ it ( 'should preserve session when app goes to background' , async ( ) => {
87+ // First connect
88+ await manager . connect ( ) ;
89+ expect ( mockWebSocketService . connect ) . toHaveBeenCalled ( ) ;
90+
91+ // Simulate app going to background
92+ const changeHandler = mockAddEventListener . mock . calls [ 0 ] [ 1 ] ;
93+ await changeHandler ( 'background' ) ;
94+
95+ // Should call disconnect(false) to preserve session
96+ expect ( mockWebSocketService . disconnect ) . toHaveBeenCalledWith ( false ) ;
97+
98+ const status = manager . getConnectionStatus ( ) ;
99+ expect ( status . isConnected ) . toBe ( false ) ;
100+ expect ( status . shouldAutoConnect ) . toBe ( true ) ;
101+ expect ( status . hasActiveSession ) . toBe ( true ) ; // Session preserved
102+ expect ( status . sessionId ) . toBe ( 'test-session-123' ) ; // Session ID preserved
103+ } ) ;
104+
105+ it ( 'should resume session when app returns to foreground' , async ( ) => {
106+ // Setup initial connection
107+ await manager . connect ( ) ;
108+ jest . clearAllMocks ( ) ;
109+
110+ // Go to background (preserve session)
111+ const changeHandler = mockAddEventListener . mock . calls [ 0 ] [ 1 ] ;
112+ await changeHandler ( 'background' ) ;
113+
114+ // Verify session preserved after background
115+ const statusAfterBackground = manager . getConnectionStatus ( ) ;
116+ expect ( statusAfterBackground . isConnected ) . toBe ( false ) ;
117+ expect ( statusAfterBackground . hasActiveSession ) . toBe ( true ) ;
118+ expect ( statusAfterBackground . sessionId ) . toBe ( 'test-session-123' ) ;
119+
120+ // Return to foreground (should resume session)
121+ await changeHandler ( 'active' ) ;
122+
123+ expect ( mockWebSocketService . connect ) . toHaveBeenCalled ( ) ;
124+ const statusAfterForeground = manager . getConnectionStatus ( ) ;
125+ expect ( statusAfterForeground . isConnected ) . toBe ( true ) ;
126+ expect ( statusAfterForeground . hasActiveSession ) . toBe ( true ) ;
127+ expect ( statusAfterForeground . sessionId ) . toBe ( 'test-session-123' ) ;
128+ } ) ;
129+
130+ it ( 'should not reconnect if it was never connected before' , async ( ) => {
131+ const changeHandler = mockAddEventListener . mock . calls [ 0 ] [ 1 ] ;
132+
133+ // Go to background without ever connecting
134+ await changeHandler ( 'background' ) ;
135+
136+ // Return to foreground
137+ await changeHandler ( 'active' ) ;
138+
139+ expect ( mockWebSocketService . connect ) . not . toHaveBeenCalled ( ) ;
140+ const status = manager . getConnectionStatus ( ) ;
141+ expect ( status . shouldAutoConnect ) . toBe ( false ) ;
142+ expect ( status . hasActiveSession ) . toBe ( false ) ;
143+ } ) ;
144+
145+ it ( 'should ignore inactive state changes' , async ( ) => {
146+ const changeHandler = mockAddEventListener . mock . calls [ 0 ] [ 1 ] ;
147+
148+ await changeHandler ( 'inactive' ) ;
149+
150+ expect ( mockWebSocketService . disconnect ) . not . toHaveBeenCalled ( ) ;
151+ expect ( mockWebSocketService . connect ) . not . toHaveBeenCalled ( ) ;
152+ } ) ;
153+
154+ it ( 'should handle reconnection failure gracefully while preserving session intent' , async ( ) => {
155+ // Setup initial connection
156+ await manager . connect ( ) ;
157+
158+ // Mock connection failure for reconnection attempt
159+ mockWebSocketService . connect . mockRejectedValueOnce ( new Error ( 'Connection failed' ) ) ;
160+
161+ const changeHandler = mockAddEventListener . mock . calls [ 0 ] [ 1 ] ;
162+
163+ // Go to background
164+ await changeHandler ( 'background' ) ;
165+
166+ // Return to foreground (should attempt reconnect but fail)
167+ await changeHandler ( 'active' ) ;
168+
169+ expect ( mockWebSocketService . connect ) . toHaveBeenCalled ( ) ;
170+ const status = manager . getConnectionStatus ( ) ;
171+ expect ( status . isConnected ) . toBe ( false ) ;
172+ expect ( status . shouldAutoConnect ) . toBe ( true ) ;
173+ expect ( status . hasActiveSession ) . toBe ( true ) ; // Should still preserve session for future attempts
174+ } ) ;
175+ } ) ;
176+
177+ describe ( 'manual disconnect method' , ( ) => {
178+ it ( 'should clear session when manually disconnecting' , async ( ) => {
179+ await manager . connect ( ) ;
180+
181+ await manager . disconnect ( ) ;
182+
183+ // Should call disconnect(true) to clear session
184+ expect ( mockWebSocketService . disconnect ) . toHaveBeenCalledWith ( true ) ;
185+
186+ const status = manager . getConnectionStatus ( ) ;
187+ expect ( status . isConnected ) . toBe ( false ) ;
188+ expect ( status . shouldAutoConnect ) . toBe ( false ) ;
189+ expect ( status . hasActiveSession ) . toBe ( false ) ; // Session cleared
190+ } ) ;
191+ } ) ;
192+
193+ describe ( 'sendMessage method' , ( ) => {
194+ it ( 'should send message when connected and in foreground' , async ( ) => {
195+ await manager . connect ( ) ;
196+
197+ const message = { event : 'test' , data : { } } ;
198+ await manager . sendMessage ( message ) ;
199+
200+ expect ( mockWebSocketService . sendMessage ) . toHaveBeenCalledWith ( message ) ;
201+ } ) ;
202+
203+ it ( 'should throw error when app is not in foreground' , async ( ) => {
204+ // Mock app being in background
205+ ( AppState as any ) . currentState = 'background' ;
206+ manager = new AppStateWebSocketManager ( ) ;
207+ manager . setWebSocketService ( mockWebSocketService ) ;
208+
209+ const message = { event : 'test' , data : { } } ;
210+
211+ await expect ( manager . sendMessage ( message ) ) . rejects . toThrow (
212+ 'Cannot send message when app is not in foreground'
213+ ) ;
214+ } ) ;
215+
216+ it ( 'should throw error when not connected' , async ( ) => {
217+ const message = { event : 'test' , data : { } } ;
218+
219+ await expect ( manager . sendMessage ( message ) ) . rejects . toThrow (
220+ 'WebSocket not connected'
221+ ) ;
222+ } ) ;
223+ } ) ;
224+
225+ describe ( 'cleanup' , ( ) => {
226+ it ( 'should remove app state listener on cleanup' , ( ) => {
227+ const mockRemove = jest . fn ( ) ;
228+ mockAddEventListener . mockReturnValue ( { remove : mockRemove } as any ) ;
229+
230+ manager = new AppStateWebSocketManager ( ) ;
231+ manager . setWebSocketService ( mockWebSocketService ) ;
232+ manager . cleanup ( ) ;
233+
234+ expect ( mockRemove ) . toHaveBeenCalled ( ) ;
235+ } ) ;
236+ } ) ;
237+
238+ describe ( 'getConnectionStatus' , ( ) => {
239+ it ( 'should return complete connection status including session info' , async ( ) => {
240+ await manager . connect ( ) ;
241+
242+ const status = manager . getConnectionStatus ( ) ;
243+ expect ( status . isConnected ) . toBe ( true ) ;
244+ expect ( status . appState ) . toBe ( 'active' ) ;
245+ expect ( status . shouldAutoConnect ) . toBe ( true ) ;
246+ expect ( status . hasActiveSession ) . toBe ( true ) ;
247+ expect ( status . sessionId ) . toBe ( 'test-session-123' ) ;
248+ } ) ;
249+
250+ it ( 'should handle missing getSessionId method gracefully' , async ( ) => {
251+ // Mock service without getSessionId method
252+ const serviceWithoutSessionId = {
253+ connect : jest . fn ( ) ,
254+ disconnect : jest . fn ( ) ,
255+ sendMessage : jest . fn ( ) ,
256+ } as unknown as MobileBackendWebSocketService ;
257+
258+ manager = new AppStateWebSocketManager ( ) ;
259+ manager . setWebSocketService ( serviceWithoutSessionId ) ;
260+
261+ const status = manager . getConnectionStatus ( ) ;
262+ expect ( status . sessionId ) . toBe ( null ) ;
263+ } ) ;
264+ } ) ;
265+ } ) ;
0 commit comments