diff options
Diffstat (limited to 'spec/frontend/drawio/drawio_editor_spec.js')
-rw-r--r-- | spec/frontend/drawio/drawio_editor_spec.js | 479 |
1 files changed, 479 insertions, 0 deletions
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js new file mode 100644 index 00000000000..d7d75922e1e --- /dev/null +++ b/spec/frontend/drawio/drawio_editor_spec.js @@ -0,0 +1,479 @@ +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { + DRAWIO_EDITOR_URL, + DRAWIO_FRAME_ID, + DIAGRAM_BACKGROUND_COLOR, + DRAWIO_IFRAME_TIMEOUT, + DIAGRAM_MAX_SIZE, +} from '~/drawio/constants'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; + +jest.mock('~/alert'); + +jest.useFakeTimers(); + +describe('drawio/drawio_editor', () => { + let editorFacade; + let drawioIFrameReceivedMessages; + const diagramURL = `${window.location.origin}/uploads/diagram.drawio.svg`; + const testSvg = '<svg></svg>'; + const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`; + const filename = 'diagram.drawio.svg'; + + const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID); + const waitForDrawioIFrameMessage = ({ messageNumber = 1 } = {}) => + new Promise((resolve) => { + let messageCounter = 0; + const iframe = findDrawioIframe(); + + iframe?.contentWindow.addEventListener('message', (event) => { + drawioIFrameReceivedMessages.push(event); + + messageCounter += 1; + + if (messageCounter === messageNumber) { + resolve(); + } + }); + }); + const expectDrawioIframeMessage = ({ expectation, messageNumber = 1 }) => { + expect(drawioIFrameReceivedMessages).toHaveLength(messageNumber); + expect(JSON.parse(drawioIFrameReceivedMessages[messageNumber - 1].data)).toEqual(expectation); + }; + const postMessageToParentWindow = (data) => { + const event = new Event('message'); + + Object.setPrototypeOf(event, { + source: findDrawioIframe().contentWindow, + data: JSON.stringify(data), + }); + + window.dispatchEvent(event); + }; + + beforeEach(() => { + editorFacade = { + getDiagram: jest.fn(), + uploadDiagram: jest.fn(), + insertDiagram: jest.fn(), + updateDiagram: jest.fn(), + }; + drawioIFrameReceivedMessages = []; + }); + + afterEach(() => { + jest.clearAllMocks(); + findDrawioIframe()?.remove(); + }); + + describe('initializing', () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + }); + + it('creates the drawio editor iframe and attaches it to the body', () => { + expect(findDrawioIframe().getAttribute('src')).toBe(DRAWIO_EDITOR_URL); + }); + + it('sets drawio-editor classname to the iframe', () => { + expect(findDrawioIframe().classList).toContain('drawio-editor'); + }); + }); + + describe(`when parent window does not receive configure event after ${DRAWIO_IFRAME_TIMEOUT} ms`, () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + }); + + it('disposes draw.io iframe', () => { + expect(findDrawioIframe()).not.toBe(null); + jest.runAllTimers(); + expect(findDrawioIframe()).toBe(null); + }); + + it('displays an alert indicating that the draw.io editor could not be loaded', () => { + jest.runAllTimers(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'The diagrams.net editor could not be loaded.', + }); + }); + }); + + describe('when parent window receives configure event', () => { + beforeEach(async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'configure' }); + + await waitForDrawioIFrameMessage(); + }); + + it('sends configure action to the draw.io iframe', () => { + expectDrawioIframeMessage({ + expectation: { + action: 'configure', + config: { + darkColor: '#202020', + settingsName: 'gitlab', + }, + colorSchemeMeta: false, + }, + }); + }); + + it('does not remove the iframe after the load error timeouts run', () => { + jest.runAllTimers(); + + expect(findDrawioIframe()).not.toBe(null); + }); + }); + + describe('when parent window receives init event', () => { + describe('when there isn’t a diagram selected', () => { + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce(null); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('sends load action to the draw.io iframe with empty svg and title', async () => { + await waitForDrawioIFrameMessage(); + + expectDrawioIframeMessage({ + expectation: { + action: 'load', + xml: null, + border: 8, + background: DIAGRAM_BACKGROUND_COLOR, + dark: false, + title: null, + }, + }); + }); + }); + + describe('when there is a diagram selected', () => { + const diagramSvg = '<svg></svg>'; + + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce({ + diagramURL, + diagramSvg, + filename, + contentType: 'image/svg+xml', + }); + + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'init' }); + }); + + it('sends load action to the draw.io iframe with the selected diagram svg and filename', async () => { + await waitForDrawioIFrameMessage(); + + // Step 5: The draw.io editor will send the downloaded diagram to the iframe + expectDrawioIframeMessage({ + expectation: { + action: 'load', + xml: diagramSvg, + border: 8, + background: DIAGRAM_BACKGROUND_COLOR, + dark: false, + title: filename, + }, + }); + }); + + it('sets the drawio iframe as visible and resets cursor', async () => { + await waitForDrawioIFrameMessage(); + + expect(findDrawioIframe().style.visibility).toBe('visible'); + expect(findDrawioIframe().style.cursor).toBe(''); + }); + + it('scrolls window to the top', async () => { + await waitForDrawioIFrameMessage(); + + expect(window.scrollX).toBe(0); + }); + }); + + describe.each` + description | errorMessage | diagram + ${'when there is an image selected that is not an svg file'} | ${'The selected image is not a valid SVG diagram'} | ${{ + diagramURL, + contentType: 'image/png', + filename: 'image.png', +}} + ${'when the selected image is not an asset upload'} | ${'The selected image is not an asset uploaded in the application'} | ${{ + diagramSvg: '<svg></svg>', + filename, + contentType: 'image/svg+xml', + diagramURL: 'https://example.com/image.drawio.svg', +}} + ${'when the selected image is too large'} | ${'The selected image is too large.'} | ${{ + diagramSvg: 'x'.repeat(DIAGRAM_MAX_SIZE + 1), + filename, + contentType: 'image/svg+xml', + diagramURL, +}} + `('$description', ({ errorMessage, diagram }) => { + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce(diagram); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('displays an error alert indicating that the image is not a diagram', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: errorMessage, + error: expect.any(Error), + }); + }); + + it('disposes the draw.io diagram iframe', () => { + expect(findDrawioIframe()).toBe(null); + }); + }); + + describe('when loading a diagram fails', () => { + beforeEach(() => { + editorFacade.getDiagram.mockRejectedValueOnce(new Error()); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('displays an error alert indicating the failure', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'Cannot load the diagram into the diagrams.net editor', + error: expect.any(Error), + }); + }); + + it('disposes the draw.io diagram iframe', () => { + expect(findDrawioIframe()).toBe(null); + }); + }); + }); + + describe('when parent window receives prompt event', () => { + describe('when the filename is empty', () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'prompt', value: '' }); + }); + + it('sends prompt action to the draw.io iframe requesting a filename', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 1 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'prompt', + titleKey: 'filename', + okKey: 'save', + defaultValue: 'diagram.drawio.svg', + }, + messageNumber: 1, + }); + }); + + it('sends dialog action to the draw.io iframe indicating that the filename cannot be empty', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 2 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'dialog', + titleKey: 'error', + messageKey: 'filenameShort', + buttonKey: 'ok', + }, + messageNumber: 2, + }); + }); + }); + + describe('when the event data is not empty', () => { + beforeEach(async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'prompt', value: 'diagram.drawio.svg' }); + + await waitForDrawioIFrameMessage(); + }); + + it('starts the saving file process', () => { + expectDrawioIframeMessage({ + expectation: { + action: 'spinner', + show: true, + messageKey: 'saving', + }, + }); + }); + }); + }); + + describe('when parent receives export event', () => { + beforeEach(() => { + editorFacade.uploadDiagram.mockResolvedValueOnce({}); + }); + + it('reloads diagram in the draw.io editor', async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage(); + + expectDrawioIframeMessage({ + expectation: expect.objectContaining({ + action: 'load', + xml: expect.stringContaining(testSvg), + }), + }); + }); + + it('marks the diagram as modified in the draw.io editor', async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 2 }); + + expectDrawioIframeMessage({ + expectation: expect.objectContaining({ + action: 'status', + modified: true, + }), + messageNumber: 2, + }); + }); + + describe('when the diagram filename is set', () => { + const TEST_FILENAME = 'diagram.drawio.svg'; + + beforeEach(() => { + launchDrawioEditor({ editorFacade, filename: TEST_FILENAME }); + }); + + it('displays loading spinner in the draw.io editor', async () => { + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'spinner', + show: true, + messageKey: 'saving', + }, + messageNumber: 3, + }); + }); + + it('uploads exported diagram', async () => { + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expect(editorFacade.uploadDiagram).toHaveBeenCalledWith({ + filename: TEST_FILENAME, + diagramSvg: expect.stringContaining(testSvg), + }); + }); + + describe('when uploading the exported diagram succeeds', () => { + it('displays an alert indicating that the diagram was uploaded successfully', async () => { + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expect(createAlert).toHaveBeenCalledWith({ + message: expect.any(String), + variant: VARIANT_SUCCESS, + fadeTransition: true, + }); + }); + + it('disposes iframe', () => { + jest.runAllTimers(); + + expect(findDrawioIframe()).toBe(null); + }); + }); + + describe('when uploading the exported diagram fails', () => { + const uploadError = new Error(); + + beforeEach(() => { + editorFacade.uploadDiagram.mockReset(); + editorFacade.uploadDiagram.mockRejectedValue(uploadError); + + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + }); + + it('hides loading indicator in the draw.io editor', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 4 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'spinner', + show: false, + }, + messageNumber: 4, + }); + }); + + it('displays an error dialog in the draw.io editor', async () => { + await waitForDrawioIFrameMessage({ messageNumber: 5 }); + + expectDrawioIframeMessage({ + expectation: { + action: 'dialog', + titleKey: 'error', + modified: true, + buttonKey: 'close', + messageKey: 'errorSavingFile', + }, + messageNumber: 5, + }); + }); + }); + }); + + describe('when diagram filename is not set', () => { + it('sends prompt action to the draw.io iframe', async () => { + launchDrawioEditor({ editorFacade }); + postMessageToParentWindow({ event: 'export', data: testEncodedSvg }); + + await waitForDrawioIFrameMessage({ messageNumber: 3 }); + + expect(drawioIFrameReceivedMessages[2].data).toEqual( + JSON.stringify({ + action: 'prompt', + titleKey: 'filename', + okKey: 'save', + defaultValue: 'diagram.drawio.svg', + }), + ); + }); + }); + }); + + describe('when parent window receives exit event', () => { + beforeEach(() => { + launchDrawioEditor({ editorFacade }); + }); + + it('disposes the the draw.io iframe', () => { + expect(findDrawioIframe()).not.toBe(null); + + postMessageToParentWindow({ event: 'exit' }); + + expect(findDrawioIframe()).toBe(null); + }); + }); +}); |