3 TIME_TO_WAIT_BEFORE_CLOSING_CONNECTION_AFTER_DOCUMENT_HIDES,
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 '../Domain/Result/Result'
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
23 metricService = new MetricService({} as Api)
25 connection = new WebsocketConnection(
27 onFailToGetToken: jest.fn(),
28 onConnecting: jest.fn(),
29 } as unknown as WebsocketCallbacks,
35 } as unknown as LoggerInterface,
39 Object.defineProperty(document, 'visibilityState', {
51 describe('DebugConnection', () => {
52 it('should not be enabled', () => {
53 expect(DebugConnection.enabled).toBe(false)
57 describe('handleVisibilityChangeEvent', () => {
58 it('should queue reconnection with no delay if visibility state is visible', () => {
59 Object.defineProperty(document, 'visibilityState', {
64 const reconnect = (connection.queueReconnection = jest.fn())
66 connection.handleVisibilityChangeEvent()
68 expect(reconnect).toHaveBeenCalledWith({ skipDelay: true })
71 it('should queue disconnect if visibility state is hidden and socket is open', () => {
74 Object.defineProperty(document, 'visibilityState', {
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()
88 it('should cancel queued disconnect if visibility state is visible', () => {
91 Object.defineProperty(document, 'visibilityState', {
96 connection.handleVisibilityChangeEvent()
98 expect(connection.closeConnectionDueToGoingAwayTimer).not.toBeUndefined()
100 Object.defineProperty(document, 'visibilityState', {
105 connection.handleVisibilityChangeEvent()
107 expect(connection.closeConnectionDueToGoingAwayTimer).toBeUndefined()
110 it('should cancel reconnection timeout if present', () => {
113 connection.reconnectTimeout = setTimeout(() => {}, 1000)
115 Object.defineProperty(document, 'visibilityState', {
120 connection.handleVisibilityChangeEvent()
122 expect(connection.reconnectTimeout).toBeUndefined()
126 describe('disconnect', () => {
127 it('should cancel reconnection timeout if present', () => {
130 connection.reconnectTimeout = setTimeout(() => {}, 1000)
132 connection.disconnect(1)
134 expect(connection.reconnectTimeout).toBeUndefined()
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 })
148 describe('connect', () => {
149 it('should not proceed if last visibility state is hidden', () => {
150 Object.defineProperty(document, 'visibilityState', {
155 const getToken = (connection.getTokenOrFailConnection = jest.fn())
157 void connection.connect()
159 expect(getToken).not.toHaveBeenCalled()
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()
173 describe('queueReconnection', () => {
174 it('should reconnect with delay if skipDelay is not set', () => {
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()
192 it('should reconnect without delay if skipDelay is set', () => {
195 const connect = (connection.connect = jest.fn())
197 connection.queueReconnection({ skipDelay: true })
199 jest.advanceTimersByTime(1)
201 expect(connect).toHaveBeenCalled()
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',
213 expect(result).toEqual(expectedResult)
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)
230 expect(connection.disconnect).toHaveBeenCalled()
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)
249 expect(connection.connect).toHaveBeenCalled()
252 describe('getTokenOrFailConnection', () => {
253 it('should call callbacks.onFailToGetToken if it fails', async () => {
256 const onFailToGetToken = jest.fn()
258 connection = new WebsocketConnection(
260 getUrlAndToken: () => Result.fail('error'),
261 onFailToGetToken: onFailToGetToken,
262 } as unknown as WebsocketCallbacks,
267 } as unknown as LoggerInterface,
271 await connection.getTokenOrFailConnection()
273 expect(onFailToGetToken).toHaveBeenCalled()
279 describe('canBroadcastMessages', () => {
280 it('should return false if connection is not ready to accept messages', () => {
281 expect(connection.canBroadcastMessages()).toBe(false)
284 it('should return true if socket is fully ready', () => {
285 connection.markAsReadyToAcceptMessages()
287 connection.socket = {
290 } as unknown as WebSocket
292 connection.state.didOpen()
294 expect(connection.canBroadcastMessages()).toBe(true)
298 describe('isConnected', () => {
299 it('should return false if socket is not open', () => {
300 connection.socket = {
303 } as unknown as WebSocket
305 connection.state.didOpen()
307 expect(connection.isConnected()).toBe(false)
310 it('should return true if socket is open', () => {
311 connection.socket = {
314 } as unknown as WebSocket
316 connection.state.didOpen()
318 expect(connection.isConnected()).toBe(true)
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')
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')
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')