Skip to content

Commit 7b77cde

Browse files
committed
feat: added unit tests for thorest/ws package
1 parent c0cece6 commit 7b77cde

File tree

2 files changed

+442
-0
lines changed

2 files changed

+442
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
2+
import { MozillaWebSocketClient } from '../../src/ws/MozillaWebSocketClient';
3+
import { type WebSocketListener } from '../../src/ws';
4+
5+
// Mock CloseEvent since it's not available in the test environment
6+
class MockCloseEvent extends Event {
7+
code: number;
8+
reason: string;
9+
wasClean: boolean;
10+
11+
constructor(
12+
type: string,
13+
options?: { code?: number; reason?: string; wasClean?: boolean }
14+
) {
15+
super(type);
16+
this.code = options?.code ?? 1000;
17+
this.reason = options?.reason ?? '';
18+
this.wasClean = options?.wasClean ?? true;
19+
}
20+
}
21+
22+
global.CloseEvent = MockCloseEvent as unknown as typeof CloseEvent;
23+
24+
// Create a proper mock class instead of an interface
25+
class MockWebSocket implements WebSocket {
26+
CONNECTING = 0 as const;
27+
OPEN = 1 as const;
28+
CLOSING = 2 as const;
29+
CLOSED = 3 as const;
30+
31+
onopen: ((event: Event) => void) | null = null;
32+
onclose: ((event: CloseEvent) => void) | null = null;
33+
onerror: ((event: Event) => void) | null = null;
34+
onmessage: ((event: MessageEvent) => void) | null = null;
35+
36+
readyState: number = 0;
37+
binaryType: BinaryType = 'blob';
38+
bufferedAmount = 0;
39+
extensions = '';
40+
protocol = '';
41+
url = '';
42+
43+
close = jest.fn();
44+
send = jest.fn();
45+
46+
addEventListener = jest.fn();
47+
removeEventListener = jest.fn();
48+
dispatchEvent = jest.fn((_event: Event) => true);
49+
}
50+
51+
// Create instance of mock
52+
const mockWebSocketInstance = new MockWebSocket();
53+
54+
const MockWebSocketConstructor = jest.fn().mockImplementation(() => {
55+
// Simulate connection process
56+
setTimeout(() => {
57+
mockWebSocketInstance.readyState = mockWebSocketInstance.OPEN;
58+
mockWebSocketInstance.onopen?.(new Event('open'));
59+
}, 0);
60+
return mockWebSocketInstance;
61+
});
62+
63+
/**
64+
* VeChain websocket - unit
65+
*
66+
* @group unit/websocket
67+
*/
68+
describe('MozillaWebSocketClient unit tests', () => {
69+
let client: MozillaWebSocketClient;
70+
let mockListener: WebSocketListener<unknown>;
71+
72+
beforeEach(() => {
73+
jest.clearAllMocks();
74+
jest.useFakeTimers();
75+
76+
// Reset mock instance state
77+
Object.assign(mockWebSocketInstance, {
78+
onopen: null,
79+
onclose: null,
80+
onerror: null,
81+
onmessage: null,
82+
close: jest.fn(),
83+
readyState: mockWebSocketInstance.CONNECTING
84+
});
85+
86+
global.WebSocket =
87+
MockWebSocketConstructor as unknown as typeof WebSocket;
88+
client = new MozillaWebSocketClient('ws://localhost:8669');
89+
mockListener = {
90+
onOpen: jest.fn(),
91+
onClose: jest.fn(),
92+
onError: jest.fn(),
93+
onMessage: jest.fn()
94+
};
95+
});
96+
97+
test('connection lifecycle', () => {
98+
client.addListener(mockListener);
99+
client.open({ path: '/test' });
100+
101+
expect(mockWebSocketInstance.readyState).toBe(
102+
mockWebSocketInstance.CONNECTING
103+
);
104+
105+
// Simulate connection success
106+
jest.runAllTimers();
107+
108+
expect(mockWebSocketInstance.readyState).toBe(
109+
mockWebSocketInstance.OPEN
110+
);
111+
expect(mockListener.onOpen).toHaveBeenCalled();
112+
113+
// Simulate closing
114+
client.close();
115+
mockWebSocketInstance.readyState = mockWebSocketInstance.CLOSED;
116+
mockWebSocketInstance.onclose?.(
117+
new MockCloseEvent('close', {
118+
code: 1000,
119+
reason: 'Normal closure'
120+
})
121+
);
122+
123+
expect(mockListener.onClose).toHaveBeenCalledWith(
124+
expect.objectContaining({
125+
code: 1000,
126+
reason: 'Normal closure'
127+
})
128+
);
129+
});
130+
131+
test('handles connection failure', () => {
132+
client.addListener(mockListener);
133+
134+
// Simulate connection failure
135+
MockWebSocketConstructor.mockImplementationOnce(() => {
136+
setTimeout(() => {
137+
mockWebSocketInstance.onerror?.(new Event('error'));
138+
mockWebSocketInstance.onclose?.(
139+
new MockCloseEvent('close', {
140+
code: 1006,
141+
reason: 'Connection failed'
142+
})
143+
);
144+
}, 0);
145+
return mockWebSocketInstance;
146+
});
147+
148+
client.open({ path: '/test' });
149+
jest.runAllTimers();
150+
151+
expect(mockListener.onError).toHaveBeenCalled();
152+
expect(mockListener.onClose).toHaveBeenCalledWith(
153+
expect.objectContaining({
154+
code: 1006,
155+
reason: 'Connection failed'
156+
})
157+
);
158+
});
159+
160+
test('handles message with different data types', () => {
161+
client.addListener(mockListener);
162+
client.open({ path: '/test' });
163+
jest.runAllTimers();
164+
165+
// Test string data
166+
const stringMessage = new MessageEvent('message', {
167+
data: 'test data'
168+
});
169+
mockWebSocketInstance.onmessage?.(stringMessage);
170+
expect(mockListener.onMessage).toHaveBeenCalledWith(stringMessage);
171+
172+
// Test JSON data
173+
const jsonMessage = new MessageEvent('message', {
174+
data: JSON.stringify({ type: 'event', data: { value: 123 } })
175+
});
176+
mockWebSocketInstance.onmessage?.(jsonMessage);
177+
expect(mockListener.onMessage).toHaveBeenCalledWith(jsonMessage);
178+
179+
// Test binary data
180+
const binaryMessage = new MessageEvent('message', {
181+
data: new Uint8Array([1, 2, 3]).buffer
182+
});
183+
mockWebSocketInstance.onmessage?.(binaryMessage);
184+
expect(mockListener.onMessage).toHaveBeenCalledWith(binaryMessage);
185+
});
186+
187+
test('multiple listeners handle all events correctly', () => {
188+
const secondListener = {
189+
onOpen: jest.fn(),
190+
onClose: jest.fn(),
191+
onError: jest.fn(),
192+
onMessage: jest.fn()
193+
};
194+
195+
client.addListener(mockListener);
196+
client.addListener(secondListener);
197+
client.open({ path: '/test' });
198+
199+
// Test connection open
200+
jest.runAllTimers();
201+
expect(mockListener.onOpen).toHaveBeenCalled();
202+
expect(secondListener.onOpen).toHaveBeenCalled();
203+
204+
// Test message handling
205+
const messageEvent = new MessageEvent('message', { data: 'test' });
206+
mockWebSocketInstance.onmessage?.(messageEvent);
207+
expect(mockListener.onMessage).toHaveBeenCalledWith(messageEvent);
208+
expect(secondListener.onMessage).toHaveBeenCalledWith(messageEvent);
209+
210+
// Test error handling
211+
const errorEvent = new Event('error');
212+
mockWebSocketInstance.onerror?.(errorEvent);
213+
expect(mockListener.onError).toHaveBeenCalledWith(errorEvent);
214+
expect(secondListener.onError).toHaveBeenCalledWith(errorEvent);
215+
});
216+
217+
test('removeListener stops event propagation', () => {
218+
client.addListener(mockListener);
219+
client.open({ path: '/test' });
220+
jest.runAllTimers();
221+
222+
const messageEvent = new MessageEvent('message', { data: 'test' });
223+
mockWebSocketInstance.onmessage?.(messageEvent);
224+
expect(mockListener.onMessage).toHaveBeenCalledWith(messageEvent);
225+
226+
// Remove listener and verify no more events are received
227+
client.removeListener(mockListener);
228+
const newMessage = new MessageEvent('message', { data: 'test2' });
229+
mockWebSocketInstance.onmessage?.(newMessage);
230+
expect(mockListener.onMessage).toHaveBeenCalledTimes(1); // Only from the first message
231+
});
232+
233+
test('reconnection behavior', () => {
234+
client.open({ path: '/test1' });
235+
jest.runAllTimers();
236+
237+
const firstConnection = mockWebSocketInstance.close;
238+
client.open({ path: '/test2' });
239+
240+
expect(firstConnection).toHaveBeenCalled();
241+
expect(MockWebSocketConstructor).toHaveBeenCalledTimes(2);
242+
expect(MockWebSocketConstructor).toHaveBeenLastCalledWith(
243+
'ws://localhost:8669/test2'
244+
);
245+
});
246+
});

0 commit comments

Comments
 (0)