Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / Services / Websockets / Debouncer / UpdateDebouncer.spec.ts
blobca70fde8716c0dc1754b5ac0df43e9dcb133a54e
1 import { DecryptedValue } from '@proton/docs-proto'
2 import type { NodeMeta } from '@proton/drive-store/lib'
3 import { UpdateDebouncer } from './UpdateDebouncer'
4 import { DocumentDebounceMode } from './DocumentDebounceMode'
5 import type { LoggerInterface } from '@proton/utils/logs'
7 describe('UpdateDebouncer', () => {
8   let debouncer: UpdateDebouncer
9   let onEvent = jest.fn()
11   beforeEach(() => {
12     debouncer = new UpdateDebouncer(
13       {} as NodeMeta,
14       {
15         info: jest.fn(),
16         debug: jest.fn(),
17       } as unknown as LoggerInterface,
18       onEvent,
19     )
21     debouncer.isReadyToFlush = true
22   })
24   afterEach(() => {
25     debouncer.destroy()
26     jest.clearAllMocks()
27   })
29   describe('markAsReadyToFlush', () => {
30     it('should set isReadyToFlush to true', () => {
31       debouncer.isReadyToFlush = false
33       debouncer.markAsReadyToFlush()
35       expect(debouncer.isReadyToFlush).toBe(true)
36     })
38     it('should flush', () => {
39       debouncer.flush = jest.fn()
41       debouncer.markAsReadyToFlush()
43       expect(debouncer.flush).toHaveBeenCalledTimes(1)
44     })
45   })
47   describe('addUpdate', () => {
48     it('should add to buffer regardless of mode', () => {
49       debouncer.setMode(DocumentDebounceMode.SinglePlayer)
50       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
52       expect(debouncer.buffer.length).toBe(1)
54       debouncer.setMode(DocumentDebounceMode.Realtime)
55       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
57       expect(debouncer.buffer.length).toBe(2)
58     })
60     it('should post WillFlush event', () => {
61       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
63       expect(onEvent).toHaveBeenCalledTimes(1)
64     })
66     it('should clear existing single player idle timeout', () => {
67       debouncer.setMode(DocumentDebounceMode.SinglePlayer)
69       jest.useFakeTimers()
71       const previousTimeout = debouncer.singlePlayerIdleTimeout
73       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
75       expect(debouncer.singlePlayerIdleTimeout).not.toBe(previousTimeout)
76     })
78     it('should flush after single player debounce period', () => {
79       jest.useFakeTimers()
81       debouncer.flush = jest.fn()
83       debouncer.setMode(DocumentDebounceMode.SinglePlayer)
84       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
86       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod)
88       expect(debouncer.flush).toHaveBeenCalledTimes(1)
89     })
91     it('should increment current buffer size', () => {
92       const data = new Uint8Array([1, 2, 3, 4, 5])
93       debouncer.addUpdates([new DecryptedValue(data)])
95       expect(debouncer.currentBufferSize).toBe(data.byteLength)
96     })
98     it('should not flush if size limit is not exceeded', () => {
99       debouncer.flush = jest.fn()
101       const maxSize = debouncer.maxSizeBeforeFlush
103       debouncer.addUpdates([new DecryptedValue(new Uint8Array(maxSize - 1))])
105       expect(debouncer.flush).not.toHaveBeenCalled()
106     })
108     it('should flush if size limit is exceeded', () => {
109       debouncer.flush = jest.fn()
111       const maxSize = debouncer.maxSizeBeforeFlush
113       debouncer.addUpdates([new DecryptedValue(new Uint8Array(maxSize))])
115       expect(debouncer.flush).toHaveBeenCalledTimes(1)
116     })
117   })
119   describe('hasPendingUpdates', () => {
120     it('should return false if buffer is empty', () => {
121       expect(debouncer.hasPendingUpdates()).toBe(false)
122     })
124     it('should return true if buffer has updates', () => {
125       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
127       expect(debouncer.hasPendingUpdates()).toBe(true)
128     })
129   })
131   describe('flush', () => {
132     it('should do nothing if buffer is empty', () => {
133       debouncer.flush()
135       expect(onEvent).not.toHaveBeenCalled()
136     })
138     it('should abort if not ready to flush', () => {
139       debouncer.isReadyToFlush = false
140       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
142       onEvent = jest.fn()
143       debouncer.flush()
145       expect(onEvent).not.toHaveBeenCalled()
146     })
148     it('should reset the current buffer size', () => {
149       debouncer.addUpdates([new DecryptedValue(new Uint8Array([1]))])
151       expect(debouncer.currentBufferSize).toBeGreaterThan(0)
153       debouncer.flush()
155       expect(debouncer.currentBufferSize).toBe(0)
156     })
158     it('should flush buffer', () => {
159       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
160       debouncer.flush()
162       expect(onEvent).toHaveBeenCalledTimes(2)
163       expect(debouncer.buffer.length).toBe(0)
164     })
165   })
167   describe('setMode', () => {
168     it('should set realtime mode', () => {
169       debouncer.setMode(DocumentDebounceMode.Realtime)
171       expect(debouncer.getMode()).toBe(DocumentDebounceMode.Realtime)
172     })
174     it('should set singleplayer mode', () => {
175       debouncer.setMode(DocumentDebounceMode.SinglePlayer)
177       expect(debouncer.getMode()).toBe(DocumentDebounceMode.SinglePlayer)
178     })
180     it('should set realtime streaming interval if realtime mode', () => {
181       expect(debouncer.realtimeStreamingInterval).not.toBeTruthy()
183       debouncer.setMode(DocumentDebounceMode.Realtime)
185       expect(debouncer.realtimeStreamingInterval).toBeTruthy()
186     })
188     it('should clear realtime streaming interval if not realtime mode', () => {
189       debouncer.setMode(DocumentDebounceMode.Realtime)
191       expect(debouncer.realtimeStreamingInterval).toBeTruthy()
193       debouncer.setMode(DocumentDebounceMode.SinglePlayer)
195       expect(debouncer.realtimeStreamingInterval).not.toBeTruthy()
196     })
197   })
199   describe('realtime mode', () => {
200     it('should flush after realtime streaming period', () => {
201       jest.useFakeTimers()
203       debouncer.flush = jest.fn()
205       debouncer.setMode(DocumentDebounceMode.Realtime)
207       jest.advanceTimersByTime(debouncer.realtimeStreamingPeriod)
209       expect(debouncer.flush).toHaveBeenCalledTimes(1)
210     })
211   })
213   describe('singleplayer mode', () => {
214     it('should flush after debounce period', () => {
215       jest.useFakeTimers()
217       debouncer.flush = jest.fn()
219       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
221       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod)
223       expect(debouncer.flush).toHaveBeenCalledTimes(1)
224     })
226     it('should not flush if debounce period passes but updates are still being added', () => {
227       jest.useFakeTimers()
229       debouncer.flush = jest.fn()
231       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
233       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod - 100)
235       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
237       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod - 100)
239       expect(debouncer.flush).not.toHaveBeenCalled()
240     })
242     it('debounce period should reset after adding a new update', () => {
243       debouncer.flush = jest.fn()
245       jest.useFakeTimers()
247       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
249       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod / 2)
251       debouncer.addUpdates([new DecryptedValue(new Uint8Array())])
253       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod / 2)
255       expect(debouncer.flush).not.toHaveBeenCalled()
257       jest.advanceTimersByTime(debouncer.singlePlayerDebouncePeriod / 2)
259       expect(debouncer.flush).toHaveBeenCalledTimes(1)
260     })
261   })