debian-mirror-gitlab/spec/frontend/drawio/drawio_editor_spec.js
2023-05-27 22:25:52 +05:30

479 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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', async () => {
expectDrawioIframeMessage({
expectation: {
action: 'configure',
config: {
darkColor: '#202020',
settingsName: 'gitlab',
},
colorSchemeMeta: false,
},
});
});
it('does not remove the iframe after the load error timeouts run', async () => {
jest.runAllTimers();
expect(findDrawioIframe()).not.toBe(null);
});
});
describe('when parent window receives init event', () => {
describe('when there isnt 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', async () => {
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', async () => {
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);
});
});
});