diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-23 06:09:23 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-23 06:09:23 +0000 |
commit | 8b4276f873461953ee5a1fc46f084779f5847e3a (patch) | |
tree | cc3435570e15234453e711c2ddcc9b0895d87eb4 /spec/frontend | |
parent | f34b26bb882947bcc1126de19fa55eb8763af32e (diff) | |
download | gitlab-ce-8b4276f873461953ee5a1fc46f084779f5847e3a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
9 files changed, 788 insertions, 19 deletions
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js index 1fdddce3962..52af3264f33 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js @@ -1,12 +1,12 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { NodeViewWrapper } from '@tiptap/vue-2'; -import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables'; +import { selectedRect as getSelectedRect } from '@tiptap/pm/tables'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils'; -jest.mock('@_ueberdosis/prosemirror-tables'); +jest.mock('@tiptap/pm/tables'); describe('content/components/wrappers/table_cell_base', () => { let wrapper; diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js index 5df901e0f15..bf29d4bdf23 100644 --- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js +++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js @@ -1,4 +1,4 @@ -import { DOMSerializer } from 'prosemirror-model'; +import { DOMSerializer } from '@tiptap/pm/model'; import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTiptapEditor } from 'jest/content_editor/test_utils'; diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js new file mode 100644 index 00000000000..de93d042396 --- /dev/null +++ b/spec/frontend/drawio/drawio_editor_spec.js @@ -0,0 +1,443 @@ +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { + DRAWIO_EDITOR_URL, + DRAWIO_FRAME_ID, + DIAGRAM_BACKGROUND_COLOR, + DRAWIO_IFRAME_TIMEOUT, +} from '~/drawio/constants'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; + +jest.mock('~/flash'); + +jest.useFakeTimers(); + +describe('drawio/drawio_editor', () => { + let editorFacade; + let drawioIFrameReceivedMessages; + const testSvg = '<svg></svg>'; + const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`; + + 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); + }); + }); + + 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 draw.io 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 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>'; + const filename = 'diagram.drawio.svg'; + + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce({ + 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, + }, + }); + }); + }); + + describe('when there is an image selected that is not a diagram', () => { + beforeEach(() => { + editorFacade.getDiagram.mockResolvedValueOnce({ + contentType: 'image/png', + filename: 'image.png', + }); + + launchDrawioEditor({ editorFacade }); + + postMessageToParentWindow({ event: 'init' }); + }); + + it('displays an error alert indicating that the image is not a diagram', async () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'The selected image is not a diagram', + 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 draw.io 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); + }); + }); +}); diff --git a/spec/frontend/drawio/markdown_field_editor_facade_spec.js b/spec/frontend/drawio/markdown_field_editor_facade_spec.js new file mode 100644 index 00000000000..992dcf0017c --- /dev/null +++ b/spec/frontend/drawio/markdown_field_editor_facade_spec.js @@ -0,0 +1,147 @@ +import AxiosMockAdapter from 'axios-mock-adapter'; +import { create } from '~/drawio/markdown_field_editor_facade'; +import * as textMarkdown from '~/lib/utils/text_markdown'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; + +jest.mock('~/lib/utils/text_markdown'); + +describe('drawio/textareaMarkdownEditor', () => { + let textArea; + let textareaMarkdownEditor; + let axiosMock; + + const markdownPreviewPath = '/markdown/preview'; + const imageURL = '/assets/image.png'; + const diagramMarkdown = '![](image.png)'; + const diagramSvg = '<svg></svg>'; + const contentType = 'image/svg+xml'; + const filename = 'image.png'; + const newDiagramMarkdown = '![](newdiagram.svg)'; + const uploadsPath = '/uploads'; + + beforeEach(() => { + textArea = document.createElement('textarea'); + textareaMarkdownEditor = create({ textArea, markdownPreviewPath, uploadsPath }); + + document.body.appendChild(textArea); + }); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + textArea.remove(); + }); + + describe('getDiagram', () => { + describe('when there is a selected diagram', () => { + beforeEach(() => { + textMarkdown.resolveSelectedImage.mockReturnValueOnce({ + imageURL, + imageMarkdown: diagramMarkdown, + filename, + }); + axiosMock + .onGet(imageURL) + .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType }); + }); + + it('returns diagram information', async () => { + const diagram = await textareaMarkdownEditor.getDiagram(); + + expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith( + textArea, + markdownPreviewPath, + ); + + expect(diagram).toEqual({ + diagramMarkdown, + filename, + diagramSvg, + contentType, + }); + }); + }); + + describe('when there is not a selected diagram', () => { + beforeEach(() => { + textMarkdown.resolveSelectedImage.mockReturnValueOnce(null); + }); + + it('returns null', async () => { + const diagram = await textareaMarkdownEditor.getDiagram(); + + expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith( + textArea, + markdownPreviewPath, + ); + + expect(diagram).toBe(null); + }); + }); + }); + + describe('updateDiagram', () => { + beforeEach(() => { + jest.spyOn(textArea, 'focus'); + jest.spyOn(textArea, 'dispatchEvent'); + + textArea.value = `diagram ${diagramMarkdown}`; + + textareaMarkdownEditor.updateDiagram({ + diagramMarkdown, + uploadResults: { link: { markdown: newDiagramMarkdown } }, + }); + }); + + it('focuses the textarea', () => { + expect(textArea.focus).toHaveBeenCalled(); + }); + + it('replaces previous diagram markdown with new diagram markdown', () => { + expect(textArea.value).toBe(`diagram ${newDiagramMarkdown}`); + }); + + it('dispatches input event in the textarea', () => { + expect(textArea.dispatchEvent).toHaveBeenCalledWith(new Event('input')); + }); + }); + + describe('insertDiagram', () => { + it('inserts markdown text and replaces any selected markdown in the textarea', () => { + textArea.value = `diagram ${diagramMarkdown}`; + textArea.setSelectionRange(0, 8); + + textareaMarkdownEditor.insertDiagram({ + uploadResults: { link: { markdown: newDiagramMarkdown } }, + }); + + expect(textMarkdown.insertMarkdownText).toHaveBeenCalledWith({ + textArea, + text: textArea.value, + tag: newDiagramMarkdown, + selected: textArea.value.substring(0, 8), + }); + }); + }); + + describe('uploadDiagram', () => { + it('sends a post request to the uploadsPath containing the diagram svg', async () => { + const link = { markdown: '![](diagram.drawio.svg)' }; + const blob = new Blob([diagramSvg], { type: 'image/svg+xml' }); + const formData = new FormData(); + + formData.append('file', blob, filename); + + axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, { + link, + }); + + const response = await textareaMarkdownEditor.uploadDiagram({ diagramSvg, filename }); + + expect(response).toEqual({ link }); + }); + }); +}); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 7aab1013fc0..2180ea7e6c2 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -1,12 +1,16 @@ import $ from 'jquery'; +import AxiosMockAdapter from 'axios-mock-adapter'; import { insertMarkdownText, keypressNoteText, compositionStartNoteText, compositionEndNoteText, updateTextForToolbarBtn, + resolveSelectedImage, } from '~/lib/utils/text_markdown'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import '~/lib/utils/jquery_at_who'; +import axios from '~/lib/utils/axios_utils'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; describe('init markdown', () => { @@ -14,6 +18,7 @@ describe('init markdown', () => { let textArea; let indentButton; let outdentButton; + let axiosMock; beforeAll(() => { setHTMLFixture( @@ -34,6 +39,14 @@ describe('init markdown', () => { document.execCommand = jest.fn(() => false); }); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + afterAll(() => { resetHTMLFixture(); }); @@ -707,6 +720,55 @@ describe('init markdown', () => { }); }); + describe('resolveSelectedImage', () => { + const markdownPreviewPath = '/markdown/preview'; + const imageMarkdown = '![image](/uploads/image.png)'; + const imageAbsoluteUrl = '/abs/uploads/image.png'; + + describe('when textarea cursor is positioned on an image', () => { + beforeEach(() => { + axiosMock.onPost(markdownPreviewPath, { text: imageMarkdown }).reply(HTTP_STATUS_OK, { + body: ` + <p><a href="${imageAbsoluteUrl}"><img src="${imageAbsoluteUrl}"></a></p> + `, + }); + }); + + it('returns the image absolute URL, markdown, and filename', async () => { + textArea.value = `image ${imageMarkdown}`; + textArea.setSelectionRange(8, 8); + expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toEqual({ + imageURL: imageAbsoluteUrl, + imageMarkdown, + filename: 'image.png', + }); + }); + }); + + describe('when textarea cursor is not positioned on an image', () => { + it.each` + markdown | selectionRange + ${`image ${imageMarkdown}`} | ${[4, 4]} + ${`!2 (issue)`} | ${[2, 2]} + `('returns null', async ({ markdown, selectionRange }) => { + textArea.value = markdown; + textArea.setSelectionRange(...selectionRange); + expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null); + }); + }); + + describe('when textarea cursor is positioned between images', () => { + it('returns null', async () => { + const position = imageMarkdown.length + 1; + + textArea.value = `${imageMarkdown}\n\n${imageMarkdown}`; + textArea.setSelectionRange(position, position); + + expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null); + }); + }); + }); + describe('Source Editor', () => { let editor; diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index fec300ddd7e..7eb0468c5be 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -28,10 +28,13 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = data-uploads-path="" > <markdown-header-stub + data-testid="markdownHeader" enablepreview="true" linecontent="" + markdownpreviewpath="foo/" restrictedtoolbaritems="" suggestionstartindex="0" + uploadspath="" /> <div diff --git a/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js new file mode 100644 index 00000000000..4daf26b0509 --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js @@ -0,0 +1,67 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue'; +import { launchDrawioEditor } from '~/drawio/drawio_editor'; +import { create } from '~/drawio/markdown_field_editor_facade'; + +jest.mock('~/drawio/drawio_editor'); +jest.mock('~/drawio/markdown_field_editor_facade'); + +describe('vue_shared/components/markdown/drawio_toolbar_button', () => { + let wrapper; + let textArea; + const uploadsPath = '/uploads'; + const markdownPreviewPath = '/markdown/preview'; + + const buildWrapper = (props = { uploadsPath, markdownPreviewPath }) => { + wrapper = shallowMount(DrawioToolbarButton, { + propsData: { + ...props, + }, + }); + }; + + beforeEach(() => { + textArea = document.createElement('textarea'); + textArea.classList.add('js-gfm-input'); + + document.body.appendChild(textArea); + }); + + afterEach(() => { + textArea.remove(); + wrapper.destroy(); + }); + + describe('default', () => { + it('renders button that launches draw.io editor', () => { + buildWrapper(); + + expect(wrapper.findComponent(GlButton).props()).toMatchObject({ + icon: 'diagram', + category: 'tertiary', + }); + }); + }); + + describe('when clicking button', () => { + it('launches draw.io editor', async () => { + const editorFacadeStub = {}; + + create.mockReturnValueOnce(editorFacadeStub); + + buildWrapper(); + + await wrapper.findComponent(GlButton).vm.$emit('click'); + + expect(create).toHaveBeenCalledWith({ + markdownPreviewPath, + textArea, + uploadsPath, + }); + expect(launchDrawioEditor).toHaveBeenCalledWith({ + editorFacade: editorFacadeStub, + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index ed417097e1e..988d9780505 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -3,6 +3,7 @@ import { nextTick } from 'vue'; import { GlTabs } from '@gitlab/ui'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; +import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; describe('Markdown field header component', () => { @@ -26,6 +27,7 @@ describe('Markdown field header component', () => { findToolbarButtons() .filter((button) => button.props(prop) === value) .at(0); + const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton); beforeEach(() => { window.gl = { @@ -197,4 +199,24 @@ describe('Markdown field header component', () => { expect(findToolbarButtons().length).toBe(defaultCount); }); }); + + describe('when drawIOEnabled is true', () => { + const uploadsPath = '/uploads'; + const markdownPreviewPath = '/preview'; + + beforeEach(() => { + createWrapper({ + drawioEnabled: true, + uploadsPath, + markdownPreviewPath, + }); + }); + + it('renders drawio toolbar button', () => { + expect(findDrawioToolbarButton().props()).toEqual({ + uploadsPath, + markdownPreviewPath, + }); + }); + }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 8acf9fd94fb..a864ae75b7f 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -82,6 +82,7 @@ export const workItemQueryResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -182,6 +183,7 @@ export const updateWorkItemMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -330,6 +332,7 @@ export const workItemResponseFactory = ({ userPermissions: { deleteWorkItem: canDelete, updateWorkItem: canUpdate, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -539,6 +542,7 @@ export const createWorkItemMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [], }, @@ -587,6 +591,7 @@ export const createWorkItemFromTaskMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [ { @@ -627,6 +632,7 @@ export const createWorkItemFromTaskMutationResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, widgets: [], }, @@ -828,15 +834,20 @@ export const workItemHierarchyEmptyResponse = { data: { workItem: { id: 'gid://gitlab/WorkItem/1', + iid: 1, + state: 'OPEN', workItemType: { - id: 'gid://gitlab/WorkItems::Type/6', + id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue', iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', + description: '', createdAt: '2022-08-03T12:41:54Z', + updatedAt: null, closedAt: null, + author: mockAssignees[0], project: { __typename: 'Project', id: '1', @@ -846,14 +857,11 @@ export const workItemHierarchyEmptyResponse = { userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, confidential: false, widgets: [ { - type: 'DESCRIPTION', - __typename: 'WorkItemWidgetDescription', - }, - { type: 'HIERARCHY', parent: null, hasChildren: false, @@ -873,6 +881,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = { data: { workItem: { id: 'gid://gitlab/WorkItem/1', + iid: 1, + state: 'OPEN', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', name: 'Issue', @@ -880,9 +890,15 @@ export const workItemHierarchyNoUpdatePermissionResponse = { __typename: 'WorkItemType', }, title: 'New title', + description: '', + createdAt: '2022-08-03T12:41:54Z', + updatedAt: null, + closedAt: null, + author: mockAssignees[0], userPermissions: { deleteWorkItem: false, updateWorkItem: false, + __typename: 'WorkItemPermissions', }, project: { __typename: 'Project', @@ -893,10 +909,6 @@ export const workItemHierarchyNoUpdatePermissionResponse = { confidential: false, widgets: [ { - type: 'DESCRIPTION', - __typename: 'WorkItemWidgetDescription', - }, - { type: 'HIERARCHY', parent: null, hasChildren: true, @@ -949,6 +961,7 @@ export const workItemTask = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [], __typename: 'WorkItem', }; @@ -966,6 +979,7 @@ export const confidentialWorkItemTask = { confidential: true, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [], __typename: 'WorkItem', }; @@ -983,6 +997,7 @@ export const closedWorkItemTask = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: '2022-08-12T13:07:52Z', + widgets: [], __typename: 'WorkItem', }; @@ -1004,6 +1019,7 @@ export const childrenWorkItems = [ confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [], __typename: 'WorkItem', }, ]; @@ -1014,15 +1030,19 @@ export const workItemHierarchyResponse = { id: 'gid://gitlab/WorkItem/1', iid: '1', workItemType: { - id: 'gid://gitlab/WorkItems::Type/6', - name: 'Objective', - iconName: 'issue-type-objective', + id: 'gid://gitlab/WorkItems::Type/1', + name: 'Issue', + iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', + }, + author: { + ...mockAssignees[0], }, confidential: false, project: { @@ -1031,12 +1051,13 @@ export const workItemHierarchyResponse = { fullPath: 'test-project-path', archived: false, }, + description: 'Issue description', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + updatedAt: null, + closedAt: null, widgets: [ { - type: 'DESCRIPTION', - __typename: 'WorkItemWidgetDescription', - }, - { type: 'HIERARCHY', parent: null, hasChildren: true, @@ -1107,6 +1128,7 @@ export const workItemObjectiveWithChild = { userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', }, author: { ...mockAssignees[0], @@ -1173,6 +1195,7 @@ export const workItemHierarchyTreeResponse = { userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', }, confidential: false, project: { @@ -1249,6 +1272,7 @@ export const changeWorkItemParentMutationResponse = { userPermissions: { deleteWorkItem: true, updateWorkItem: true, + __typename: 'WorkItemPermissions', }, description: null, id: 'gid://gitlab/WorkItem/2', @@ -1621,6 +1645,7 @@ export const projectWorkItemResponse = { workItems: { nodes: [workItemQueryResponse.data.workItem], }, + __typename: 'Project', }, }, }; |