@@ -4,6 +4,17 @@ import type { CatcherMessage } from '@hawk.so/types';
44
55const MOCK_WEBSOCKET_URL = 'ws://localhost:1234' ;
66
7+ /**
8+ * vi.fn() replacement has no WebSocket.OPEN/CLOSED; Socket uses them in switch — without this,
9+ * `undefined === undefined` always hits the first `case WebSocket.OPEN` and reconnect never runs.
10+ */
11+ function patchWebSocketMockConstructor ( ctor : { CONNECTING ?: number ; OPEN ?: number ; CLOSING ?: number ; CLOSED ?: number } ) : void {
12+ ctor . CONNECTING = 0 ;
13+ ctor . OPEN = 1 ;
14+ ctor . CLOSING = 2 ;
15+ ctor . CLOSED = 3 ;
16+ }
17+
718type MockWebSocket = {
819 url : string ;
920 readyState : number ;
@@ -72,3 +83,94 @@ describe('Socket', () => {
7283 expect ( WebSocketConstructor ) . toHaveBeenCalledTimes ( 2 ) ;
7384 } ) ;
7485} ) ;
86+
87+ /**
88+ * Regression: queued events must be flushed after reconnect / init, not only on first constructor connect.
89+ */
90+ describe ( 'Socket — events queue after connection loss' , ( ) => {
91+ afterEach ( ( ) => {
92+ vi . restoreAllMocks ( ) ;
93+ } ) ;
94+
95+ function mockWebSocketFactory ( sockets : MockWebSocket [ ] , closeSpy : ReturnType < typeof vi . fn > ) {
96+ const ctor = vi . fn < ( url : string ) => void > ( ) . mockImplementation ( function (
97+ this : MockWebSocket ,
98+ url : string
99+ ) {
100+ this . url = url ;
101+ this . readyState = WebSocket . CONNECTING ;
102+ this . send = vi . fn ( ) ;
103+ this . close = closeSpy ;
104+ this . onopen = undefined ;
105+ this . onclose = undefined ;
106+ this . onerror = undefined ;
107+ this . onmessage = undefined ;
108+ sockets . push ( this ) ;
109+ } ) ;
110+ patchWebSocketMockConstructor ( ctor ) ;
111+
112+ return ctor ;
113+ }
114+
115+ it ( 'should flush queued event after reconnect when socket is CLOSED' , async ( ) => {
116+ const sockets : MockWebSocket [ ] = [ ] ;
117+ const closeSpy = vi . fn ( function ( this : MockWebSocket ) {
118+ this . readyState = WebSocket . CLOSED ;
119+ this . onclose ?.( { code : 1001 } as CloseEvent ) ;
120+ } ) ;
121+
122+ const WebSocketConstructor = mockWebSocketFactory ( sockets , closeSpy ) ;
123+ globalThis . WebSocket = WebSocketConstructor as unknown as typeof WebSocket ;
124+
125+ const socket = new Socket ( {
126+ collectorEndpoint : MOCK_WEBSOCKET_URL ,
127+ reconnectionTimeout : 10 ,
128+ } ) ;
129+
130+ const ws1 = sockets [ 0 ] ;
131+ ws1 . readyState = WebSocket . OPEN ;
132+ ws1 . onopen ?.( new Event ( 'open' ) ) ;
133+ await Promise . resolve ( ) ;
134+
135+ ws1 . readyState = WebSocket . CLOSED ;
136+
137+ const payload = { type : 'errors/javascript' , title : 'queued-after-drop' } as unknown as CatcherMessage < 'errors/javascript' > ;
138+ const sendPromise = socket . send ( payload ) ;
139+
140+ const ws2 = sockets [ 1 ] ;
141+ expect ( ws2 ) . toBeDefined ( ) ;
142+ ws2 . readyState = WebSocket . OPEN ;
143+ ws2 . onopen ?.( new Event ( 'open' ) ) ;
144+ await sendPromise ;
145+
146+ expect ( ws2 . send ) . toHaveBeenCalledTimes ( 1 ) ;
147+ expect ( ws2 . send ) . toHaveBeenCalledWith ( JSON . stringify ( payload ) ) ;
148+ } ) ;
149+
150+ it ( 'should flush queued event when ws is null after pagehide and send()' , async ( ) => {
151+ const closeSpy = vi . fn ( function ( this : MockWebSocket ) {
152+ this . readyState = WebSocket . CLOSED ;
153+ this . onclose ?.( { code : 1000 } as CloseEvent ) ;
154+ } ) ;
155+
156+ const sockets : MockWebSocket [ ] = [ ] ;
157+ const WebSocketConstructor = mockWebSocketFactory ( sockets , closeSpy ) ;
158+ globalThis . WebSocket = WebSocketConstructor as unknown as typeof WebSocket ;
159+
160+ const socket = new Socket ( { collectorEndpoint : MOCK_WEBSOCKET_URL } ) ;
161+ sockets [ 0 ] . readyState = WebSocket . OPEN ;
162+ sockets [ 0 ] . onopen ?.( new Event ( 'open' ) ) ;
163+ await Promise . resolve ( ) ;
164+
165+ window . dispatchEvent ( new Event ( 'pagehide' ) ) ;
166+
167+ const queued = { foo : 'bar' } as unknown as CatcherMessage < 'errors/javascript' > ;
168+ const sendPromise = socket . send ( queued ) ;
169+ sockets [ 1 ] . readyState = WebSocket . OPEN ;
170+ sockets [ 1 ] . onopen ?.( new Event ( 'open' ) ) ;
171+ await sendPromise ;
172+
173+ expect ( sockets [ 1 ] . send ) . toHaveBeenCalledTimes ( 1 ) ;
174+ expect ( sockets [ 1 ] . send ) . toHaveBeenCalledWith ( JSON . stringify ( queued ) ) ;
175+ } ) ;
176+ } ) ;
0 commit comments