Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / EditorController / EditorController.spec.ts
blobcc2737d994b4573517d467413f8a14f211e2822e
1 import { EditorController } from './EditorController'
2 import type { Logger } from '@proton/utils/logs'
3 import { DocumentRole } from '@proton/docs-shared'
4 import type { ConnectionCloseReason } from '@proton/docs-proto'
5 import { EventType, EventTypeEnum } from '@proton/docs-proto'
6 import metrics from '@proton/metrics'
7 import type { DocumentStateValues } from '../State/DocumentState'
8 import { DocumentState } from '../State/DocumentState'
9 import type { ClientRequiresEditorMethods, DecryptedMessage } from '@proton/docs-shared'
10 import type { SerializedEditorState } from 'lexical'
11 import type { DecryptedCommit } from '../Models/DecryptedCommit'
13 jest.mock('@proton/metrics', () => ({
14   docs_readonly_mode_documents_total: {
15     increment: jest.fn(),
16   },
17 }))
19 describe('EditorController', () => {
20   let controller: EditorController
21   let logger: Logger
22   let exportAndDownload: any
23   let sharedState: DocumentState
24   let editorInvoker: jest.Mocked<ClientRequiresEditorMethods>
26   beforeEach(() => {
27     logger = {
28       info: jest.fn(),
29       error: jest.fn(),
30       warn: jest.fn(),
31       debug: jest.fn(),
32     } as unknown as Logger
34     exportAndDownload = {
35       execute: jest.fn(),
36     }
38     sharedState = new DocumentState({
39       ...DocumentState.defaults,
40       userRole: new DocumentRole('Editor'),
41     } as DocumentStateValues)
43     editorInvoker = {
44       receiveMessage: jest.fn(),
45       showEditor: jest.fn(),
46       performOpeningCeremony: jest.fn(),
47       performClosingCeremony: jest.fn(),
48       changeLockedState: jest.fn(),
49       broadcastPresenceState: jest.fn(),
50       toggleDebugTreeView: jest.fn(),
51       printAsPDF: jest.fn(),
52       getCurrentEditorState: jest.fn(),
53       showCommentsPanel: jest.fn(),
54       exportData: jest.fn(),
55       getDocumentState: jest.fn(),
56       getClientId: jest.fn(),
57       replaceEditorState: jest.fn(),
58     } as unknown as jest.Mocked<ClientRequiresEditorMethods>
60     controller = new EditorController(logger, exportAndDownload, sharedState)
61   })
63   describe('receiveEditor', () => {
64     it('should initialize editor invoker and set editor ready state', () => {
65       controller.receiveEditor(editorInvoker)
66       expect(sharedState.getProperty('editorReady')).toBe(true)
67       expect(logger.info).toHaveBeenCalledWith('Editor is ready to receive invocations')
68     })
69   })
71   describe('showEditorForTheFirstTime', () => {
72     beforeEach(() => {
73       controller.receiveEditor(editorInvoker)
74     })
76     it('should throw error if editor invoker is not initialized', () => {
77       const controller = new EditorController(logger, exportAndDownload, sharedState)
78       expect(() => controller.showEditorForTheFirstTime()).toThrow('Editor invoker not initialized')
79     })
81     it('should show editor when realtime is disabled', () => {
82       sharedState.setProperty('realtimeEnabled', false)
83       sharedState.setProperty('realtimeReadyToBroadcast', false)
84       sharedState.setProperty('realtimeConnectionTimedOut', false)
86       controller.showEditorForTheFirstTime()
88       expect(editorInvoker.showEditor).toHaveBeenCalled()
89       expect(editorInvoker.performOpeningCeremony).toHaveBeenCalled()
90     })
92     it('should show editor when realtime is ready to broadcast', () => {
93       sharedState.setProperty('realtimeEnabled', true)
94       sharedState.setProperty('realtimeReadyToBroadcast', true)
95       sharedState.setProperty('realtimeConnectionTimedOut', false)
97       controller.showEditorForTheFirstTime()
99       expect(editorInvoker.showEditor).toHaveBeenCalled()
100       expect(editorInvoker.performOpeningCeremony).toHaveBeenCalled()
101     })
103     it('should show editor when realtime connection has timed out', () => {
104       sharedState.setProperty('realtimeEnabled', true)
105       sharedState.setProperty('realtimeReadyToBroadcast', false)
106       sharedState.setProperty('realtimeConnectionTimedOut', true)
108       controller.showEditorForTheFirstTime()
110       expect(editorInvoker.showEditor).toHaveBeenCalled()
111       expect(editorInvoker.performOpeningCeremony).toHaveBeenCalled()
112     })
114     it('should not show editor when realtime is enabled but not ready and not timed out', () => {
115       sharedState.setProperty('realtimeEnabled', true)
116       sharedState.setProperty('realtimeReadyToBroadcast', false)
117       sharedState.setProperty('realtimeConnectionTimedOut', false)
119       controller.showEditorForTheFirstTime()
121       expect(editorInvoker.showEditor).not.toHaveBeenCalled()
122       expect(editorInvoker.performOpeningCeremony).not.toHaveBeenCalled()
123     })
125     it('should emit EditorIsReadyToBeShown event when editor is shown', () => {
126       sharedState.setProperty('realtimeEnabled', false)
127       sharedState.setProperty('realtimeReadyToBroadcast', false)
128       sharedState.setProperty('realtimeConnectionTimedOut', false)
129       sharedState.emitEvent = jest.fn()
131       controller.showEditorForTheFirstTime()
133       expect(sharedState.emitEvent).toHaveBeenCalledWith({
134         name: 'EditorIsReadyToBeShown',
135         payload: undefined,
136       })
137     })
139     it('should reload editing locked state when editor is shown', () => {
140       sharedState.setProperty('realtimeEnabled', true)
141       sharedState.setProperty('realtimeReadyToBroadcast', true)
143       controller.reloadEditingLockedState = jest.fn()
145       controller.showEditorForTheFirstTime()
147       expect(controller.reloadEditingLockedState).toHaveBeenCalled()
148     })
149   })
151   describe('reloadEditingLockedState', () => {
152     beforeEach(() => {
153       controller.receiveEditor(editorInvoker)
154     })
156     it('should do nothing if editor invoker is not initialized', () => {
157       const controller = new EditorController(logger, exportAndDownload, sharedState)
158       controller.reloadEditingLockedState()
159       expect(editorInvoker.changeLockedState).not.toHaveBeenCalled()
160     })
162     it('should lock editor when participant limit is reached and user is not admin', () => {
163       sharedState.setProperty('realtimeIsParticipantLimitReached', true)
164       sharedState.setProperty('userRole', new DocumentRole('Editor'))
166       controller.reloadEditingLockedState()
168       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
169       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
170         reason: 'user_limit_reached',
171       })
172     })
174     it('should lock editor when user lacks edit permissions', () => {
175       sharedState.setProperty('userRole', new DocumentRole('Viewer'))
177       controller.reloadEditingLockedState()
179       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
180       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
181         reason: 'no_editing_permissions',
182       })
183     })
185     it('should lock editor when experiencing errored sync', () => {
186       sharedState.setProperty('userRole', new DocumentRole('Editor'))
187       sharedState.setProperty('realtimeIsExperiencingErroredSync', true)
189       controller.reloadEditingLockedState()
191       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
192       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
193         reason: 'errored_sync',
194       })
195     })
197     it('should lock editor when size constraint is reached', () => {
198       sharedState.setProperty('userRole', new DocumentRole('Editor'))
199       sharedState.setProperty('realtimeIsLockedDueToSizeContraint', true)
201       controller.reloadEditingLockedState()
203       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
204       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
205         reason: 'size_limit',
206       })
207     })
209     it('should lock editor when not connected', () => {
210       sharedState.setProperty('userRole', new DocumentRole('Editor'))
211       sharedState.setProperty('realtimeStatus', 'disconnected')
213       controller.reloadEditingLockedState()
215       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
216       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
217         reason: 'not_connected',
218       })
219     })
221     it('should unlock editor when all conditions are met', () => {
222       sharedState.setProperty('userRole', new DocumentRole('Editor'))
223       sharedState.setProperty('realtimeStatus', 'connected')
224       sharedState.setProperty('documentTrashState', 'not_trashed')
225       sharedState.setProperty('editorHasRenderingIssue', false)
226       sharedState.setProperty('realtimeIsLockedDueToSizeContraint', false)
227       sharedState.setProperty('realtimeIsExperiencingErroredSync', false)
229       controller.reloadEditingLockedState()
231       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(false)
232     })
234     it('should lock editor when document is in trash', () => {
235       sharedState.setProperty('realtimeStatus', 'connected')
236       controller.receiveEditor(editorInvoker)
237       sharedState.setProperty('userRole', new DocumentRole('Editor'))
238       sharedState.setProperty('documentTrashState', 'trashed')
240       controller.reloadEditingLockedState()
242       expect(logger.info).toHaveBeenCalledWith('Locking editor due to trash state')
243       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
244       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalled()
245     })
247     it('should not lock editor when document is not in trash', () => {
248       controller.receiveEditor(editorInvoker)
249       sharedState.setProperty('userRole', new DocumentRole('Editor'))
250       sharedState.setProperty('documentTrashState', 'not_trashed')
251       sharedState.setProperty('realtimeStatus', 'connected')
253       controller.reloadEditingLockedState()
255       expect(logger.info).toHaveBeenCalledWith('Unlocking editor')
256       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(false)
257     })
258   })
260   describe('event handling', () => {
261     beforeEach(() => {
262       controller.receiveEditor(editorInvoker)
263     })
265     it('should handle realtime document updates', () => {
266       const payload = { content: new Uint8Array([1, 2, 3]) } as DecryptedMessage
267       sharedState.emitEvent({
268         name: 'RealtimeReceivedDocumentUpdate',
269         payload,
270       })
272       expect(editorInvoker.receiveMessage).toHaveBeenCalledWith({
273         type: { wrapper: 'du' },
274         content: payload.content,
275       })
276     })
278     it('should handle client presence state', () => {
279       const payload = new Uint8Array([1, 2, 3])
280       sharedState.emitEvent({
281         name: 'RealtimeReceivedOtherClientPresenceState',
282         payload,
283       })
285       expect(editorInvoker.receiveMessage).toHaveBeenCalledWith({
286         type: {
287           wrapper: 'events',
288           eventType: EventType.create(EventTypeEnum.ClientIsBroadcastingItsPresenceState).value,
289         },
290         content: payload,
291       })
292     })
294     it('should handle connection closed', () => {
295       sharedState.emitEvent({
296         name: 'RealtimeConnectionClosed',
297         payload: {} as ConnectionCloseReason,
298       })
300       expect(editorInvoker.performClosingCeremony).toHaveBeenCalled()
301     })
302   })
304   describe('export and document operations', () => {
305     beforeEach(() => {
306       controller.receiveEditor(editorInvoker)
307     })
309     it('should export and download document', async () => {
310       const format = 'docx'
311       const documentName = 'test.docx'
312       const data = new Uint8Array([1, 2, 3])
314       editorInvoker.exportData.mockResolvedValue(data)
315       sharedState.setProperty('documentName', documentName)
317       await controller.exportAndDownload(format)
319       expect(editorInvoker.exportData).toHaveBeenCalledWith(format)
320       expect(exportAndDownload.execute).toHaveBeenCalledWith(data, documentName, format)
321     })
323     it('should throw when exporting without editor invoker', async () => {
324       const controller = new EditorController(logger, exportAndDownload, sharedState)
325       await expect(controller.exportAndDownload('docx')).rejects.toThrow()
326     })
328     it('should restore revision by replacing', async () => {
329       const lexicalState = { root: {} } as SerializedEditorState
330       await controller.restoreRevisionByReplacing(lexicalState)
331       expect(editorInvoker.replaceEditorState).toHaveBeenCalledWith(lexicalState)
332     })
334     it('should get document client id', async () => {
335       const clientId = 123
336       editorInvoker.getClientId.mockResolvedValue(clientId)
338       const result = await controller.getDocumentClientId()
339       expect(result).toBe(clientId)
340     })
341   })
343   describe('subscription handlers', () => {
344     beforeEach(() => {
345       controller.receiveEditor(editorInvoker)
346     })
348     it('should show editor when realtimeReadyToBroadcast becomes true', () => {
349       sharedState.setProperty('realtimeReadyToBroadcast', true)
350       expect(editorInvoker.showEditor).toHaveBeenCalled()
351       expect(editorInvoker.performOpeningCeremony).toHaveBeenCalled()
352     })
354     it('should send base commit to editor when baseCommit changes', () => {
355       const mockContent = new Uint8Array([1, 2, 3])
356       sharedState.setProperty('baseCommit', {
357         squashedRepresentation: () => mockContent,
358       } as unknown as DecryptedCommit)
360       expect(editorInvoker.receiveMessage).toHaveBeenCalledWith({
361         type: { wrapper: 'du' },
362         content: mockContent,
363         origin: 'InitialLoad',
364       })
365     })
367     it('should broadcast presence state when requested', () => {
368       sharedState.emitEvent({
369         name: 'RealtimeRequestingClientToBroadcastItsState',
370         payload: undefined,
371       })
372       expect(editorInvoker.broadcastPresenceState).toHaveBeenCalled()
373     })
374   })
376   describe('sendBaseCommitToEditor', () => {
377     it('should do nothing if baseCommit is not set', () => {
378       sharedState.setProperty('baseCommit', undefined)
379       controller.sendBaseCommitToEditor()
380       expect(editorInvoker.receiveMessage).not.toHaveBeenCalled()
381     })
382   })
384   describe('additional editor state methods', () => {
385     beforeEach(() => {
386       controller.receiveEditor(editorInvoker)
387     })
389     it('should handle broadcastPresenceState', () => {
390       controller.broadcastPresenceState()
391       expect(editorInvoker.broadcastPresenceState).toHaveBeenCalled()
392     })
394     it('should throw when calling broadcastPresenceState without editor', () => {
395       const controller = new EditorController(logger, exportAndDownload, sharedState)
396       expect(() => controller.broadcastPresenceState()).toThrow('Editor invoker not initialized')
397     })
399     it('should handle getEditorJSON', async () => {
400       const mockState = { root: {} } as SerializedEditorState
401       editorInvoker.getCurrentEditorState.mockResolvedValue(mockState)
403       const result = await controller.getEditorJSON()
404       expect(result).toEqual(mockState)
405       expect(editorInvoker.getCurrentEditorState).toHaveBeenCalled()
406     })
408     it('should handle getDocumentState', async () => {
409       const mockState = new Uint8Array([1, 2, 3])
410       editorInvoker.getDocumentState.mockResolvedValue(mockState)
412       const result = await controller.getDocumentState()
413       expect(result).toEqual(mockState)
414     })
415   })
417   describe('reloadEditingLockedState additional cases', () => {
418     beforeEach(() => {
419       controller.receiveEditor(editorInvoker)
420     })
422     it('should lock editor when editor has rendering issue', () => {
423       sharedState.setProperty('userRole', new DocumentRole('Editor'))
424       sharedState.setProperty('editorHasRenderingIssue', true)
426       controller.reloadEditingLockedState()
428       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
429     })
431     it('should lock editor when document is trashed', () => {
432       sharedState.setProperty('userRole', new DocumentRole('Editor'))
433       sharedState.setProperty('documentTrashState', 'trashed')
435       controller.reloadEditingLockedState()
437       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
438     })
439   })
441   describe('metrics', () => {
442     beforeEach(() => {
443       controller.receiveEditor(editorInvoker)
444     })
446     it('should increment metrics with unknown reason when no specific condition is met', () => {
447       sharedState.setProperty('userRole', new DocumentRole('Editor'))
448       sharedState.setProperty('realtimeStatus', 'connected')
450       controller.incrementMetricsReadonlyState()
452       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
453         reason: 'unknown',
454       })
455     })
457     it('should increment metrics with not_connected reason', () => {
458       sharedState.setProperty('userRole', new DocumentRole('Editor'))
459       sharedState.setProperty('realtimeStatus', 'disconnected')
461       controller.incrementMetricsReadonlyState()
463       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
464         reason: 'not_connected',
465       })
466     })
467   })
469   describe('showCommentsPanel', () => {
470     it('should do nothing if editor is not initialized', () => {
471       const controller = new EditorController(logger, exportAndDownload, sharedState)
472       controller.showCommentsPanel()
473       expect(editorInvoker.showCommentsPanel).not.toHaveBeenCalled()
474     })
476     it('should show comments panel when editor is initialized', () => {
477       controller.receiveEditor(editorInvoker)
478       controller.showCommentsPanel()
479       expect(editorInvoker.showCommentsPanel).toHaveBeenCalled()
480     })
481   })
483   describe('toggleDebugTreeView', () => {
484     it('should throw error when editor is not initialized', async () => {
485       await expect(controller.toggleDebugTreeView()).rejects.toThrow('Editor invoker not initialized')
486     })
488     it('should call editor toggleDebugTreeView when initialized', async () => {
489       controller.receiveEditor(editorInvoker)
490       await controller.toggleDebugTreeView()
491       expect(editorInvoker.toggleDebugTreeView).toHaveBeenCalled()
492     })
493   })
495   describe('printAsPDF', () => {
496     it('should throw error when editor is not initialized', async () => {
497       await expect(controller.printAsPDF()).rejects.toThrow('Editor invoker not initialized')
498     })
500     it('should call editor printAsPDF when initialized', async () => {
501       controller.receiveEditor(editorInvoker)
502       await controller.printAsPDF()
503       expect(editorInvoker.printAsPDF).toHaveBeenCalled()
504     })
505   })
507   describe('changeLockedState', () => {
508     it('should throw error when editor is not initialized', () => {
509       expect(() => controller.changeLockedState(true)).toThrow('Editor invoker not initialized')
510     })
512     it('should call editor changeLockedState when initialized', () => {
513       controller.receiveEditor(editorInvoker)
514       controller.changeLockedState(true)
515       expect(editorInvoker.changeLockedState).toHaveBeenCalledWith(true)
516     })
517   })
519   describe('performClosingCeremony', () => {
520     it('should throw error when editor is not initialized', () => {
521       expect(() => controller.performClosingCeremony()).toThrow('Editor invoker not initialized')
522     })
524     it('should call editor performClosingCeremony when initialized', () => {
525       controller.receiveEditor(editorInvoker)
526       controller.performClosingCeremony()
527       expect(editorInvoker.performClosingCeremony).toHaveBeenCalled()
528     })
529   })
531   describe('event handling edge cases', () => {
532     it('should handle RealtimeReceivedDocumentUpdate when editor is not initialized', () => {
533       const payload = { content: new Uint8Array([1, 2, 3]) } as DecryptedMessage
534       sharedState.emitEvent({
535         name: 'RealtimeReceivedDocumentUpdate',
536         payload,
537       })
538       expect(editorInvoker.receiveMessage).not.toHaveBeenCalled()
539     })
541     it('should handle RealtimeConnectionClosed when editor is not initialized', () => {
542       sharedState.emitEvent({
543         name: 'RealtimeConnectionClosed',
544         payload: {} as ConnectionCloseReason,
545       })
546       expect(editorInvoker.performClosingCeremony).not.toHaveBeenCalled()
547     })
549     it('should handle RealtimeReceivedOtherClientPresenceState when editor is not initialized', () => {
550       const payload = new Uint8Array([1, 2, 3])
551       sharedState.emitEvent({
552         name: 'RealtimeReceivedOtherClientPresenceState',
553         payload,
554       })
555       expect(editorInvoker.receiveMessage).not.toHaveBeenCalled()
556     })
557   })
559   describe('metrics edge cases', () => {
560     it('should increment metrics with size_limit reason', () => {
561       sharedState.setProperty('userRole', new DocumentRole('Editor'))
562       sharedState.setProperty('realtimeIsLockedDueToSizeContraint', true)
564       controller.incrementMetricsReadonlyState()
566       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
567         reason: 'size_limit',
568       })
569     })
571     it('should increment metrics with errored_sync reason', () => {
572       sharedState.setProperty('userRole', new DocumentRole('Editor'))
573       sharedState.setProperty('realtimeIsExperiencingErroredSync', true)
575       controller.incrementMetricsReadonlyState()
577       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
578         reason: 'errored_sync',
579       })
580     })
582     it('should increment metrics with user_limit_reached reason', () => {
583       sharedState.setProperty('realtimeIsParticipantLimitReached', true)
585       controller.incrementMetricsReadonlyState()
587       expect(metrics.docs_readonly_mode_documents_total.increment).toHaveBeenCalledWith({
588         reason: 'user_limit_reached',
589       })
590     })
591   })
593   describe('document state operations', () => {
594     it('should throw when getting document state without editor', async () => {
595       await expect(controller.getDocumentState()).rejects.toThrow(
596         'Attempting to get document state before editor invoker is initialized',
597       )
598     })
600     it('should return undefined for client id when editor is not initialized', async () => {
601       const result = await controller.getDocumentClientId()
602       expect(result).toBeUndefined()
603     })
605     it('should throw when restoring revision without editor', async () => {
606       const lexicalState = { root: {} } as SerializedEditorState
607       await expect(controller.restoreRevisionByReplacing(lexicalState)).rejects.toThrow(
608         'Attempting to restore revision by replacing before editor invoker is initialized',
609       )
610     })
611   })
613   describe('editor ready state', () => {
614     it('should emit EditorIsReadyToBeShown event when showing editor', () => {
615       const emitEventSpy = jest.spyOn(sharedState, 'emitEvent')
616       controller.receiveEditor(editorInvoker)
617       sharedState.setProperty('realtimeEnabled', true)
618       sharedState.setProperty('realtimeReadyToBroadcast', true)
620       controller.showEditorForTheFirstTime()
622       expect(emitEventSpy).toHaveBeenCalledWith({
623         name: 'EditorIsReadyToBeShown',
624         payload: undefined,
625       })
626     })
628     it('should log info when showing editor', () => {
629       controller.receiveEditor(editorInvoker)
630       sharedState.setProperty('realtimeEnabled', true)
631       sharedState.setProperty('realtimeReadyToBroadcast', true)
633       controller.showEditorForTheFirstTime()
635       expect(logger.info).toHaveBeenCalledWith('Showing editor for the first time')
636     })
637   })
639   describe('getEditorJSON', () => {
640     it('should throw error when editor is not initialized', async () => {
641       await expect(controller.getEditorJSON()).rejects.toThrow('Editor invoker not initialized')
642     })
644     it('should return editor state when initialized', async () => {
645       const mockState: SerializedEditorState = {
646         root: {
647           children: [],
648           direction: null,
649           format: '',
650           indent: 0,
651           type: 'root',
652           version: 1,
653         },
654       }
656       controller.receiveEditor(editorInvoker)
657       editorInvoker.getCurrentEditorState.mockResolvedValue(mockState)
659       const result = await controller.getEditorJSON()
661       expect(result).toEqual(mockState)
662       expect(editorInvoker.getCurrentEditorState).toHaveBeenCalled()
663     })
664   })
666   describe('exportData', () => {
667     it('should throw error when editor is not initialized', async () => {
668       await expect(controller.exportData('docx')).rejects.toThrow(
669         'Attepting to export document before editor invoker or decrypted node is initialized',
670       )
671     })
673     it('should return exported data when initialized', async () => {
674       const mockExportedData = new Uint8Array([1, 2, 3, 4])
675       controller.receiveEditor(editorInvoker)
676       editorInvoker.exportData.mockResolvedValue(mockExportedData)
678       const result = await controller.exportData('docx')
680       expect(result).toEqual(mockExportedData)
681       expect(editorInvoker.exportData).toHaveBeenCalledWith('docx')
682     })
683   })
685   describe('RealtimeRequestingClientToBroadcastItsState event', () => {
686     it('should not broadcast presence state when editor is not initialized', () => {
687       sharedState.emitEvent({
688         name: 'RealtimeRequestingClientToBroadcastItsState',
689         payload: undefined,
690       })
692       expect(editorInvoker.broadcastPresenceState).not.toHaveBeenCalled()
693     })
695     it('should broadcast presence state when editor is initialized', () => {
696       controller.receiveEditor(editorInvoker)
698       sharedState.emitEvent({
699         name: 'RealtimeRequestingClientToBroadcastItsState',
700         payload: undefined,
701       })
703       expect(editorInvoker.broadcastPresenceState).toHaveBeenCalled()
704     })
705   })