Merge branch 'docs-header-fix' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / Realtime / WebsocketConnection.spec.ts
blob93e87d28619392a23bd530a357896b66750daf0a
1 import {
2   DebugConnection,
3   TIME_TO_WAIT_BEFORE_CLOSING_CONNECTION_AFTER_DOCUMENT_HIDES,
4   WebsocketConnection,
5 } from './WebsocketConnection'
6 import { getWebSocketServerURL } from './getWebSocketServerURL'
7 import type { LoggerInterface } from '@proton/utils/logs'
8 import type { WebsocketCallbacks } from '@proton/docs-shared'
9 import { Result } from '@proton/docs-shared'
10 import { MetricService } from '../Services/Metrics/MetricService'
11 import type { Api } from '@proton/shared/lib/interfaces'
13 const setWindowLocationHref = (href: string) => {
14   delete (window as any).location
15   ;(window as any).location = new URL(href)
18 describe('WebsocketConnection', () => {
19   let connection: WebsocketConnection
20   let metricService: MetricService
22   beforeEach(() => {
23     metricService = new MetricService({} as Api)
25     connection = new WebsocketConnection(
26       {
27         onFailToGetToken: jest.fn(),
28         onConnecting: jest.fn(),
29       } as unknown as WebsocketCallbacks,
30       metricService,
31       {
32         error: jest.fn(),
33         info: jest.fn(),
34         warn: jest.fn(),
35       } as unknown as LoggerInterface,
36       '0.0.0.0',
37     )
39     Object.defineProperty(document, 'visibilityState', {
40       value: 'visible',
41       writable: true,
42     })
43   })
45   afterEach(() => {
46     connection.destroy()
47     jest.clearAllMocks()
48     jest.useRealTimers()
49   })
51   describe('DebugConnection', () => {
52     it('should not be enabled', () => {
53       expect(DebugConnection.enabled).toBe(false)
54     })
55   })
57   describe('handleVisibilityChangeEvent', () => {
58     it('should queue reconnection with no delay if visibility state is visible', () => {
59       Object.defineProperty(document, 'visibilityState', {
60         value: 'visible',
61         writable: true,
62       })
64       const reconnect = (connection.queueReconnection = jest.fn())
66       connection.handleVisibilityChangeEvent()
68       expect(reconnect).toHaveBeenCalledWith({ skipDelay: true })
69     })
71     it('should queue disconnect if visibility state is hidden and socket is open', () => {
72       jest.useFakeTimers()
74       Object.defineProperty(document, 'visibilityState', {
75         value: 'hidden',
76         writable: true,
77       })
79       const disconnect = (connection.disconnect = jest.fn())
81       connection.handleVisibilityChangeEvent()
83       jest.advanceTimersByTime(TIME_TO_WAIT_BEFORE_CLOSING_CONNECTION_AFTER_DOCUMENT_HIDES + 1)
85       expect(disconnect).toHaveBeenCalled()
86     })
88     it('should cancel queued disconnect if visibility state is visible', () => {
89       jest.useFakeTimers()
91       Object.defineProperty(document, 'visibilityState', {
92         value: 'hidden',
93         writable: true,
94       })
96       connection.handleVisibilityChangeEvent()
98       expect(connection.closeConnectionDueToGoingAwayTimer).not.toBeUndefined()
100       Object.defineProperty(document, 'visibilityState', {
101         value: 'visible',
102         writable: true,
103       })
105       connection.handleVisibilityChangeEvent()
107       expect(connection.closeConnectionDueToGoingAwayTimer).toBeUndefined()
108     })
110     it('should cancel reconnection timeout if present', () => {
111       jest.useFakeTimers()
113       connection.reconnectTimeout = setTimeout(() => {}, 1000)
115       Object.defineProperty(document, 'visibilityState', {
116         value: 'hidden',
117         writable: true,
118       })
120       connection.handleVisibilityChangeEvent()
122       expect(connection.reconnectTimeout).toBeUndefined()
123     })
124   })
126   describe('disconnect', () => {
127     it('should cancel reconnection timeout if present', () => {
128       jest.useFakeTimers()
130       connection.reconnectTimeout = setTimeout(() => {}, 1000)
132       connection.disconnect(1)
134       expect(connection.reconnectTimeout).toBeUndefined()
135     })
136   })
138   describe('handleWindowCameOnlineEvent', () => {
139     it('should reconnect without delay', () => {
140       const reconnect = (connection.queueReconnection = jest.fn())
142       connection.handleWindowCameOnlineEvent()
144       expect(reconnect).toHaveBeenCalledWith({ skipDelay: true })
145     })
146   })
148   describe('connect', () => {
149     it('should not proceed if last visibility state is hidden', () => {
150       Object.defineProperty(document, 'visibilityState', {
151         value: 'hidden',
152         writable: true,
153       })
155       const getToken = (connection.getTokenOrFailConnection = jest.fn())
157       void connection.connect()
159       expect(getToken).not.toHaveBeenCalled()
160     })
162     it('should abort if abort signal returns true', async () => {
163       const abortSignal = () => true
165       connection.getTokenOrFailConnection = jest.fn().mockReturnValue(Result.ok({ token: '123' }))
167       await connection.connect(abortSignal)
169       expect(connection.callbacks.onConnecting).not.toHaveBeenCalled()
170     })
171   })
173   describe('queueReconnection', () => {
174     it('should reconnect with delay if skipDelay is not set', () => {
175       jest.useFakeTimers()
177       const connect = (connection.connect = jest.fn())
179       connection.state.getBackoff = jest.fn().mockReturnValue(1000)
181       connection.queueReconnection()
183       jest.advanceTimersByTime(500)
185       expect(connect).not.toHaveBeenCalled()
187       jest.advanceTimersByTime(1000)
189       expect(connect).toHaveBeenCalled()
190     })
192     it('should reconnect without delay if skipDelay is set', () => {
193       jest.useFakeTimers()
195       const connect = (connection.connect = jest.fn())
197       connection.queueReconnection({ skipDelay: true })
199       jest.advanceTimersByTime(1)
201       expect(connect).toHaveBeenCalled()
202     })
203   })
205   it('should correctly format url', () => {
206     const expectedResult = 'wss://docs-rts.darwin.proton.black/websockets/?token=123'
208     const result = connection.buildConnectionUrl({
209       serverUrl: 'wss://docs-rts.darwin.proton.black/websockets',
210       token: '123',
211     })
213     expect(result).toEqual(expectedResult)
214   })
216   it('should disconnect websocket when offline browser event is triggered', async () => {
217     const fn = (connection.disconnect = jest.fn())
219     window.dispatchEvent(new Event('offline'))
221     await new Promise<void>((resolve) => {
222       const interval = setInterval(() => {
223         if (fn.mock.calls.length > 0) {
224           clearInterval(interval)
225           resolve()
226         }
227       }, 10)
228     })
230     expect(connection.disconnect).toHaveBeenCalled()
231   })
233   it('should connect websocket when online browser event is triggered', async () => {
234     const fn = (connection.connect = jest.fn())
236     connection.state.getBackoff = jest.fn().mockReturnValue(0)
238     window.dispatchEvent(new Event('online'))
240     await new Promise<void>((resolve) => {
241       const interval = setInterval(() => {
242         if (fn.mock.calls.length > 0) {
243           clearInterval(interval)
244           resolve()
245         }
246       }, 10)
247     })
249     expect(connection.connect).toHaveBeenCalled()
250   })
252   describe('getTokenOrFailConnection', () => {
253     it('should call callbacks.onFailToGetToken if it fails', async () => {
254       connection.destroy()
256       const onFailToGetToken = jest.fn()
258       connection = new WebsocketConnection(
259         {
260           getUrlAndToken: () => Result.fail('error'),
261           onFailToGetToken: onFailToGetToken,
262         } as unknown as WebsocketCallbacks,
263         metricService,
264         {
265           error: jest.fn(),
266           info: jest.fn(),
267         } as unknown as LoggerInterface,
268         '0.0.0.0',
269       )
271       await connection.getTokenOrFailConnection()
273       expect(onFailToGetToken).toHaveBeenCalled()
275       connection.destroy()
276     })
277   })
279   describe('canBroadcastMessages', () => {
280     it('should return false if connection is not ready to accept messages', () => {
281       expect(connection.canBroadcastMessages()).toBe(false)
282     })
284     it('should return true if socket is fully ready', () => {
285       connection.markAsReadyToAcceptMessages()
287       connection.socket = {
288         readyState: 1,
289         close: jest.fn(),
290       } as unknown as WebSocket
292       connection.state.didOpen()
294       expect(connection.canBroadcastMessages()).toBe(true)
295     })
296   })
298   describe('isConnected', () => {
299     it('should return false if socket is not open', () => {
300       connection.socket = {
301         readyState: 0,
302         close: jest.fn(),
303       } as unknown as WebSocket
305       connection.state.didOpen()
307       expect(connection.isConnected()).toBe(false)
308     })
310     it('should return true if socket is open', () => {
311       connection.socket = {
312         readyState: 1,
313         close: jest.fn(),
314       } as unknown as WebSocket
316       connection.state.didOpen()
318       expect(connection.isConnected()).toBe(true)
319     })
320   })
322   describe('getWebSocketServerURL', () => {
323     test('should add docs-rts subdomain if there is no subdomain', () => {
324       setWindowLocationHref('https://proton.me')
325       expect(getWebSocketServerURL()).toBe('wss://docs-rts.proton.me/websockets')
326     })
328     test('should replace first subdomain with docs-rts', () => {
329       setWindowLocationHref('https://docs-editor.proton.me')
330       expect(getWebSocketServerURL()).toBe('wss://docs-rts.proton.me/websockets')
331     })
333     test('should replace first subdomain with docs-rts for multiple subdomains', () => {
334       setWindowLocationHref('https://docs.hutton.proton.black')
335       expect(getWebSocketServerURL()).toBe('wss://docs-rts.hutton.proton.black/websockets')
336     })
337   })