diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/rich_content_editor')
22 files changed, 0 insertions, 1537 deletions
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js deleted file mode 100644 index ce2b0d1ddc1..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; -import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; -import { - generateToolbarItem, - addCustomEventListener, - removeCustomEventListener, - registerHTMLToMarkdownRenderer, - addImage, - insertVideo, - getMarkdown, - getEditorOptions, -} from '~/vue_shared/components/rich_content_editor/services/editor_service'; -import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html'; - -jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'); -jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer'); -jest.mock('~/vue_shared/components/rich_content_editor/services/sanitize_html'); - -describe('Editor Service', () => { - let mockInstance; - let event; - let handler; - const parseHtml = (str) => { - const wrapper = document.createElement('div'); - wrapper.innerHTML = str; - return wrapper.firstChild; - }; - - beforeEach(() => { - mockInstance = { - eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() }, - editor: { - exec: jest.fn(), - isWysiwygMode: jest.fn(), - getSquire: jest.fn(), - insertText: jest.fn(), - }, - invoke: jest.fn(), - toMarkOptions: { - renderer: { - constructor: { - factory: jest.fn(), - }, - }, - }, - }; - event = 'someCustomEvent'; - handler = jest.fn(); - }); - - describe('generateToolbarItem', () => { - const config = { - icon: 'bold', - command: 'some-command', - tooltip: 'Some Tooltip', - event: 'some-event', - }; - - const generatedItem = generateToolbarItem(config); - - it('generates the correct command', () => { - expect(generatedItem.options.command).toBe(config.command); - }); - - it('generates the correct event', () => { - expect(generatedItem.options.event).toBe(config.event); - }); - - it('generates a divider when isDivider is set to true', () => { - const isDivider = true; - - expect(generateToolbarItem({ isDivider })).toBe('divider'); - }); - }); - - describe('addCustomEventListener', () => { - it('registers an event type on the instance and adds an event handler', () => { - addCustomEventListener(mockInstance, event, handler); - - expect(mockInstance.eventManager.addEventType).toHaveBeenCalledWith(event); - expect(mockInstance.eventManager.listen).toHaveBeenCalledWith(event, handler); - }); - }); - - describe('removeCustomEventListener', () => { - it('removes an event handler from the instance', () => { - removeCustomEventListener(mockInstance, event, handler); - - expect(mockInstance.eventManager.removeEventHandler).toHaveBeenCalledWith(event, handler); - }); - }); - - describe('addImage', () => { - const file = new File([], 'some-file.jpg'); - const mockImage = { imageUrl: 'some/url.png', altText: 'some alt text' }; - - it('calls the insertElement method on the squire instance when in WYSIWYG mode', () => { - jest.spyOn(URL, 'createObjectURL'); - mockInstance.editor.isWysiwygMode.mockReturnValue(true); - mockInstance.editor.getSquire.mockReturnValue({ insertElement: jest.fn() }); - - addImage(mockInstance, mockImage, file); - - expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalled(); - expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(file); - }); - - it('calls the insertText method on the instance when in Markdown mode', () => { - mockInstance.editor.isWysiwygMode.mockReturnValue(false); - addImage(mockInstance, mockImage, file); - - expect(mockInstance.editor.insertText).toHaveBeenCalledWith('![some alt text](some/url.png)'); - }); - }); - - describe('insertVideo', () => { - const mockUrl = 'some/url'; - const htmlString = `<figure contenteditable="false" class="gl-relative gl-h-0 video_container"><iframe class="gl-absolute gl-top-0 gl-left-0 gl-w-full gl-h-full" width="560" height="315" frameborder="0" src="some/url"></iframe></figure>`; - const mockInsertElement = jest.fn(); - - beforeEach(() => - mockInstance.editor.getSquire.mockReturnValue({ insertElement: mockInsertElement }), - ); - - describe('WYSIWYG mode', () => { - it('calls the insertElement method on the squire instance with an iFrame element', () => { - mockInstance.editor.isWysiwygMode.mockReturnValue(true); - - insertVideo(mockInstance, mockUrl); - - expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalledWith( - parseHtml(htmlString), - ); - }); - }); - - describe('Markdown mode', () => { - it('calls the insertText method on the editor instance with the iFrame element HTML', () => { - mockInstance.editor.isWysiwygMode.mockReturnValue(false); - - insertVideo(mockInstance, mockUrl); - - expect(mockInstance.editor.insertText).toHaveBeenCalledWith(htmlString); - }); - }); - }); - - describe('getMarkdown', () => { - it('calls the invoke method on the instance', () => { - getMarkdown(mockInstance); - - expect(mockInstance.invoke).toHaveBeenCalledWith('getMarkdown'); - }); - }); - - describe('registerHTMLToMarkdownRenderer', () => { - let baseRenderer; - const htmlToMarkdownRenderer = {}; - const extendedRenderer = {}; - - beforeEach(() => { - baseRenderer = mockInstance.toMarkOptions.renderer; - buildHTMLToMarkdownRenderer.mockReturnValueOnce(htmlToMarkdownRenderer); - baseRenderer.constructor.factory.mockReturnValueOnce(extendedRenderer); - - registerHTMLToMarkdownRenderer(mockInstance); - }); - - it('builds a new instance of the HTML to Markdown renderer', () => { - expect(buildHTMLToMarkdownRenderer).toHaveBeenCalledWith(baseRenderer); - }); - - it('extends base renderer with the HTML to Markdown renderer', () => { - expect(baseRenderer.constructor.factory).toHaveBeenCalledWith( - baseRenderer, - htmlToMarkdownRenderer, - ); - }); - - it('replaces the default renderer with extended renderer', () => { - expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer); - }); - }); - - describe('getEditorOptions', () => { - const externalOptions = { - customRenderers: {}, - }; - const renderer = {}; - - beforeEach(() => { - buildCustomRenderer.mockReturnValueOnce(renderer); - }); - - it('generates a configuration object with a custom HTML renderer and toolbarItems', () => { - expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer); - expect(getEditorOptions()).toHaveProp('toolbarItems'); - }); - - it('passes external renderers to the buildCustomRenderers function', () => { - getEditorOptions(externalOptions); - expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers); - }); - - it('uses the internal sanitizeHTML service for HTML sanitization', () => { - const options = getEditorOptions(); - const html = '<div></div>'; - - options.customHTMLSanitizer(html); - - expect(sanitizeHTML).toHaveBeenCalledWith(html); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js deleted file mode 100644 index 97aecda97d2..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import { GlModal, GlTabs } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constants'; -import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; -import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue'; - -describe('Add Image Modal', () => { - let wrapper; - const propsData = { imageRoot: 'path/to/root/' }; - - const findModal = () => wrapper.find(GlModal); - const findTabs = () => wrapper.find(GlTabs); - const findUploadImageTab = () => wrapper.find(UploadImageTab); - const findUrlInput = () => wrapper.find({ ref: 'urlInput' }); - const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' }); - - beforeEach(() => { - wrapper = shallowMount(AddImageModal, { propsData }); - }); - - describe('when content is loaded', () => { - it('renders a modal component', () => { - expect(findModal().exists()).toBe(true); - }); - - it('renders a Tabs component', () => { - expect(findTabs().exists()).toBe(true); - }); - - it('renders an upload image tab', () => { - expect(findUploadImageTab().exists()).toBe(true); - }); - - it('renders an input to add an image URL', () => { - expect(findUrlInput().exists()).toBe(true); - }); - - it('renders an input to add an image description', () => { - expect(findDescriptionInput().exists()).toBe(true); - }); - }); - - describe('add image', () => { - describe('Upload', () => { - it('validates the file', () => { - const preventDefault = jest.fn(); - const description = 'some description'; - const file = { name: 'some_file.png' }; - - wrapper.vm.$refs.uploadImageTab = { validateFile: jest.fn() }; - wrapper.setData({ file, description, tabIndex: IMAGE_TABS.UPLOAD_TAB }); - - findModal().vm.$emit('ok', { preventDefault }); - - expect(wrapper.vm.$refs.uploadImageTab.validateFile).toHaveBeenCalled(); - }); - }); - - describe('URL', () => { - it('emits an addImage event when a valid URL is specified', () => { - const preventDefault = jest.fn(); - const mockImage = { imageUrl: '/some/valid/url.png', description: 'some description' }; - wrapper.setData({ ...mockImage, tabIndex: IMAGE_TABS.URL_TAB }); - - findModal().vm.$emit('ok', { preventDefault }); - expect(preventDefault).not.toHaveBeenCalled(); - expect(wrapper.emitted('addImage')).toEqual([ - [{ imageUrl: mockImage.imageUrl, altText: mockImage.description }], - ]); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js deleted file mode 100644 index 81fd059ce4f..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue'; - -describe('Upload Image Tab', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallowMount(UploadImageTab); - }); - - afterEach(() => wrapper.destroy()); - - const triggerInputEvent = (size) => { - const file = { size, name: 'file-name.png' }; - const mockEvent = new Event('input'); - - Object.defineProperty(mockEvent, 'target', { value: { files: [file] } }); - - wrapper.find({ ref: 'fileInput' }).element.dispatchEvent(mockEvent); - - return file; - }; - - describe('onInput', () => { - it.each` - size | fileError - ${2000000000} | ${'Maximum file size is 2MB. Please select a smaller file.'} - ${200} | ${null} - `('validates the file correctly', ({ size, fileError }) => { - triggerInputEvent(size); - - expect(wrapper.vm.fileError).toBe(fileError); - }); - }); - - it('emits input event when file is valid', () => { - const file = triggerInputEvent(200); - - expect(wrapper.emitted('input')).toEqual([[file]]); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js deleted file mode 100644 index 3e9eaf58181..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue'; - -describe('Insert Video Modal', () => { - let wrapper; - - const findModal = () => wrapper.find(GlModal); - const findUrlInput = () => wrapper.find({ ref: 'urlInput' }); - - const triggerInsertVideo = (url) => { - const preventDefault = jest.fn(); - findUrlInput().vm.$emit('input', url); - findModal().vm.$emit('primary', { preventDefault }); - }; - - beforeEach(() => { - wrapper = shallowMount(InsertVideoModal); - }); - - afterEach(() => wrapper.destroy()); - - describe('when content is loaded', () => { - it('renders a modal component', () => { - expect(findModal().exists()).toBe(true); - }); - - it('renders an input to add a URL', () => { - expect(findUrlInput().exists()).toBe(true); - }); - }); - - describe('insert video', () => { - it.each` - url | emitted - ${'https://www.youtube.com/embed/someId'} | ${[['https://www.youtube.com/embed/someId']]} - ${'https://www.youtube.com/watch?v=1234'} | ${[['https://www.youtube.com/embed/1234']]} - ${'::youtube.com/invalid/url'} | ${undefined} - `('formats the url correctly', ({ url, emitted }) => { - triggerInsertVideo(url); - expect(wrapper.emitted('insertVideo')).toEqual(emitted); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js deleted file mode 100644 index 47b1abd2ad2..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import Editor from '@toast-ui/editor'; -import buildMarkdownToHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; -import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service'; - -describe('vue_shared/components/rich_content_editor', () => { - let editor; - - const buildEditor = () => { - editor = new Editor({ - el: document.body, - customHTMLRenderer: buildMarkdownToHTMLRenderer(), - }); - - registerHTMLToMarkdownRenderer(editor); - }; - - beforeEach(() => { - buildEditor(); - }); - - describe('HTML to Markdown', () => { - it('uses "-" character list marker in unordered lists', () => { - editor.setHtml('<ul><li>List item 1</li><li>List item 2</li></ul>'); - - const markdown = editor.getMarkdown(); - - expect(markdown).toBe('- List item 1\n- List item 2'); - }); - - it('does not increment the list marker in ordered lists', () => { - editor.setHtml('<ol><li>List item 1</li><li>List item 2</li></ol>'); - - const markdown = editor.getMarkdown(); - - expect(markdown).toBe('1. List item 1\n1. List item 2'); - }); - - it('indents lists using four spaces', () => { - editor.setHtml('<ul><li>List item 1</li><ul><li>List item 2</li></ul></ul>'); - - const markdown = editor.getMarkdown(); - - expect(markdown).toBe('- List item 1\n - List item 2'); - }); - - it('uses * for strong and _ for emphasis text', () => { - editor.setHtml('<strong>strong text</strong><i>emphasis text</i>'); - - const markdown = editor.getMarkdown(); - - expect(markdown).toBe('**strong text**_emphasis text_'); - }); - }); - - describe('Markdown to HTML', () => { - it.each` - input | output - ${'markdown with _emphasized\ntext_'} | ${'<p>markdown with <em>emphasized text</em></p>\n'} - ${'markdown with **strong\ntext**'} | ${'<p>markdown with <strong>strong text</strong></p>\n'} - `( - 'does not transform softbreaks inside (_) and strong (**) nodes into <br/> tags', - ({ input, output }) => { - editor.setMarkdown(input); - - expect(editor.getHtml()).toBe(output); - }, - ); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js deleted file mode 100644 index 8eb880b3984..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ /dev/null @@ -1,222 +0,0 @@ -import { Editor, mockEditorApi } from '@toast-ui/vue-editor'; -import { shallowMount } from '@vue/test-utils'; -import { - EDITOR_TYPES, - EDITOR_HEIGHT, - EDITOR_PREVIEW_STYLE, - CUSTOM_EVENTS, -} from '~/vue_shared/components/rich_content_editor/constants'; -import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; -import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue'; -import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; - -import { - addCustomEventListener, - removeCustomEventListener, - addImage, - insertVideo, - registerHTMLToMarkdownRenderer, - getEditorOptions, - getMarkdown, -} from '~/vue_shared/components/rich_content_editor/services/editor_service'; - -jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({ - addCustomEventListener: jest.fn(), - removeCustomEventListener: jest.fn(), - addImage: jest.fn(), - insertVideo: jest.fn(), - registerHTMLToMarkdownRenderer: jest.fn(), - getEditorOptions: jest.fn(), - getMarkdown: jest.fn(), -})); - -describe('Rich Content Editor', () => { - let wrapper; - - const content = '## Some Markdown'; - const imageRoot = 'path/to/root/'; - const findEditor = () => wrapper.find({ ref: 'editor' }); - const findAddImageModal = () => wrapper.find(AddImageModal); - const findInsertVideoModal = () => wrapper.find(InsertVideoModal); - - const buildWrapper = async () => { - wrapper = shallowMount(RichContentEditor, { - propsData: { content, imageRoot }, - stubs: { - ToastEditor: Editor, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('when content is loaded', () => { - const editorOptions = {}; - - beforeEach(() => { - getEditorOptions.mockReturnValueOnce(editorOptions); - buildWrapper(); - }); - - it('renders an editor', () => { - expect(findEditor().exists()).toBe(true); - }); - - it('renders the correct content', () => { - expect(findEditor().props().initialValue).toBe(content); - }); - - it('provides options generated by the getEditorOptions service', () => { - expect(findEditor().props().options).toBe(editorOptions); - }); - - it('has the correct preview style', () => { - expect(findEditor().props().previewStyle).toBe(EDITOR_PREVIEW_STYLE); - }); - - it('has the correct initial edit type', () => { - expect(findEditor().props().initialEditType).toBe(EDITOR_TYPES.wysiwyg); - }); - - it('has the correct height', () => { - expect(findEditor().props().height).toBe(EDITOR_HEIGHT); - }); - }); - - describe('when content is changed', () => { - beforeEach(() => { - buildWrapper(); - }); - - it('emits an input event with the changed content', () => { - const changedMarkdown = '## Changed Markdown'; - getMarkdown.mockReturnValueOnce(changedMarkdown); - - findEditor().vm.$emit('change'); - - expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown); - }); - }); - - describe('when content is reset', () => { - beforeEach(() => { - buildWrapper(); - }); - - it('should reset the content via setMarkdown', () => { - const newContent = 'Just the body content excluding the front matter for example'; - const mockInstance = { invoke: jest.fn() }; - wrapper.vm.$refs.editor = mockInstance; - - wrapper.vm.resetInitialValue(newContent); - - expect(mockInstance.invoke).toHaveBeenCalledWith('setMarkdown', newContent); - }); - }); - - describe('when editor is loaded', () => { - const formattedMarkdown = 'formatted markdown'; - - beforeEach(() => { - mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown); - buildWrapper(); - }); - - afterEach(() => { - mockEditorApi.getMarkdown.mockReset(); - }); - - it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { - expect(addCustomEventListener).toHaveBeenCalledWith( - wrapper.vm.editorApi, - CUSTOM_EVENTS.openAddImageModal, - wrapper.vm.onOpenAddImageModal, - ); - }); - - it('adds the CUSTOM_EVENTS.openInsertVideoModal custom event listener', () => { - expect(addCustomEventListener).toHaveBeenCalledWith( - wrapper.vm.editorApi, - CUSTOM_EVENTS.openInsertVideoModal, - wrapper.vm.onOpenInsertVideoModal, - ); - }); - - it('registers HTML to markdown renderer', () => { - expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi); - }); - - it('emits load event with the markdown formatted by Toast UI', () => { - mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown); - expect(mockEditorApi.getMarkdown).toHaveBeenCalled(); - expect(wrapper.emitted('load')[0]).toEqual([{ formattedMarkdown }]); - }); - }); - - describe('when editor is destroyed', () => { - beforeEach(() => { - buildWrapper(); - }); - - it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { - wrapper.vm.$destroy(); - - expect(removeCustomEventListener).toHaveBeenCalledWith( - wrapper.vm.editorApi, - CUSTOM_EVENTS.openAddImageModal, - wrapper.vm.onOpenAddImageModal, - ); - }); - - it('removes the CUSTOM_EVENTS.openInsertVideoModal custom event listener', () => { - wrapper.vm.$destroy(); - - expect(removeCustomEventListener).toHaveBeenCalledWith( - wrapper.vm.editorApi, - CUSTOM_EVENTS.openInsertVideoModal, - wrapper.vm.onOpenInsertVideoModal, - ); - }); - }); - - describe('add image modal', () => { - beforeEach(() => { - buildWrapper(); - }); - - it('renders an addImageModal component', () => { - expect(findAddImageModal().exists()).toBe(true); - }); - - it('calls the onAddImage method when the addImage event is emitted', () => { - const mockImage = { imageUrl: 'some/url.png', altText: 'some description' }; - const mockInstance = { exec: jest.fn() }; - wrapper.vm.$refs.editor = mockInstance; - - findAddImageModal().vm.$emit('addImage', mockImage); - expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage, undefined); - }); - }); - - describe('insert video modal', () => { - beforeEach(() => { - buildWrapper(); - }); - - it('renders an insertVideoModal component', () => { - expect(findInsertVideoModal().exists()).toBe(true); - }); - - it('calls the onInsertVideo method when the insertVideo event is emitted', () => { - const mockUrl = 'https://www.youtube.com/embed/someId'; - const mockInstance = { exec: jest.fn() }; - wrapper.vm.$refs.editor = mockInstance; - - findInsertVideoModal().vm.$emit('insertVideo', mockUrl); - expect(insertVideo).toHaveBeenCalledWith(mockInstance, mockUrl); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js deleted file mode 100644 index a823d04024d..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import buildCustomHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; - -describe('Build Custom Renderer Service', () => { - describe('buildCustomHTMLRenderer', () => { - it('should return an object with the default renderer functions when lacking arguments', () => { - expect(buildCustomHTMLRenderer()).toEqual( - expect.objectContaining({ - htmlBlock: expect.any(Function), - htmlInline: expect.any(Function), - heading: expect.any(Function), - item: expect.any(Function), - paragraph: expect.any(Function), - text: expect.any(Function), - softbreak: expect.any(Function), - }), - ); - }); - - it('should return an object with both custom and default renderer functions when passed customRenderers', () => { - const mockHtmlCustomRenderer = jest.fn(); - const customRenderers = { - html: [mockHtmlCustomRenderer], - }; - - expect(buildCustomHTMLRenderer(customRenderers)).toEqual( - expect.objectContaining({ - html: expect.any(Function), - }), - ); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js deleted file mode 100644 index 3caf03dabba..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js +++ /dev/null @@ -1,218 +0,0 @@ -import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; -import { attributeDefinition } from './renderers/mock_data'; - -describe('rich_content_editor/services/html_to_markdown_renderer', () => { - let baseRenderer; - let htmlToMarkdownRenderer; - let fakeNode; - - beforeEach(() => { - baseRenderer = { - trim: jest.fn((input) => `trimmed ${input}`), - getSpaceCollapsedText: jest.fn((input) => `space collapsed ${input}`), - getSpaceControlled: jest.fn((input) => `space controlled ${input}`), - convert: jest.fn(), - }; - - fakeNode = { nodeValue: 'mock_node', dataset: {} }; - }); - - afterEach(() => { - htmlToMarkdownRenderer = null; - }); - - describe('TEXT_NODE visitor', () => { - it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => { - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - - expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe( - `space controlled trimmed space collapsed ${fakeNode.nodeValue}`, - ); - }); - }); - - describe('LI OL, LI UL visitor', () => { - const oneLevelNestedList = '\n * List item 1\n * List item 2'; - const twoLevelNestedList = '\n * List item 1\n * List item 2'; - const spaceInContentList = '\n * List item 1\n * List item 2'; - - it.each` - list | indentSpaces | result - ${oneLevelNestedList} | ${2} | ${'\n * List item 1\n * List item 2'} - ${oneLevelNestedList} | ${3} | ${'\n * List item 1\n * List item 2'} - ${oneLevelNestedList} | ${6} | ${'\n * List item 1\n * List item 2'} - ${twoLevelNestedList} | ${4} | ${'\n * List item 1\n * List item 2'} - ${spaceInContentList} | ${1} | ${'\n * List item 1\n * List item 2'} - `('changes the list indentation to $indentSpaces spaces', ({ list, indentSpaces, result }) => { - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { - subListIndentSpaces: indentSpaces, - }); - - baseRenderer.convert.mockReturnValueOnce(list); - - expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list); - }); - }); - - describe('UL LI visitor', () => { - it.each` - listItem | unorderedListBulletChar | result | bulletChar - ${'* list item'} | ${undefined} | ${'- list item'} | ${'default'} - ${' - list item'} | ${'*'} | ${' * list item'} | ${'*'} - ${' * list item'} | ${'-'} | ${' - list item'} | ${'-'} - `( - 'uses $bulletChar bullet char in unordered list items when $unorderedListBulletChar is set in config', - ({ listItem, unorderedListBulletChar, result }) => { - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { - unorderedListBulletChar, - }); - baseRenderer.convert.mockReturnValueOnce(listItem); - - expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem); - }, - ); - - it('detects attribute definitions and attaches them to the list item', () => { - const listItem = '- list item'; - const result = `${listItem}\n${attributeDefinition}\n`; - - fakeNode.dataset.attributeDefinition = attributeDefinition; - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`); - - expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result); - }); - }); - - describe('OL LI visitor', () => { - it.each` - listItem | result | incrementListMarker | action - ${'2. list item'} | ${'1. list item'} | ${false} | ${'increments'} - ${' 3. list item'} | ${' 1. list item'} | ${false} | ${'increments'} - ${' 123. list item'} | ${' 1. list item'} | ${false} | ${'increments'} - ${'3. list item'} | ${'3. list item'} | ${true} | ${'does not increment'} - `( - '$action a list item counter when incrementListMaker is $incrementListMarker', - ({ listItem, result, incrementListMarker }) => { - const subContent = null; - - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { - incrementListMarker, - }); - baseRenderer.convert.mockReturnValueOnce(listItem); - - expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent); - }, - ); - }); - - describe('STRONG, B visitor', () => { - it.each` - input | strongCharacter | result - ${'**strong text**'} | ${'_'} | ${'__strong text__'} - ${'__strong text__'} | ${'*'} | ${'**strong text**'} - `( - 'converts $input to $result when strong character is $strongCharacter', - ({ input, strongCharacter, result }) => { - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { - strong: strongCharacter, - }); - - baseRenderer.convert.mockReturnValueOnce(input); - - expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); - }, - ); - }); - - describe('EM, I visitor', () => { - it.each` - input | emphasisCharacter | result - ${'*strong text*'} | ${'_'} | ${'_strong text_'} - ${'_strong text_'} | ${'*'} | ${'*strong text*'} - `( - 'converts $input to $result when emphasis character is $emphasisCharacter', - ({ input, emphasisCharacter, result }) => { - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, { - emphasis: emphasisCharacter, - }); - - baseRenderer.convert.mockReturnValueOnce(input); - - expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); - }, - ); - }); - - describe('H1, H2, H3, H4, H5, H6 visitor', () => { - it('detects attribute definitions and attaches them to the heading', () => { - const heading = 'heading text'; - const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`; - - fakeNode.dataset.attributeDefinition = attributeDefinition; - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`); - - expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result); - }); - }); - - describe('PRE CODE', () => { - let node; - const subContent = 'sub content'; - const originalConverterResult = 'base result'; - - beforeEach(() => { - node = document.createElement('PRE'); - - node.innerText = 'reference definition content'; - node.dataset.sseReferenceDefinition = true; - - baseRenderer.convert.mockReturnValueOnce(originalConverterResult); - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - }); - - it('returns raw text when pre node has sse-reference-definitions class', () => { - expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe( - `\n\n${node.innerText}\n\n`, - ); - }); - - it('returns base result when pre node does not have sse-reference-definitions class', () => { - delete node.dataset.sseReferenceDefinition; - - expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult); - }); - }); - - describe('IMG', () => { - const originalSrc = 'path/to/image.png'; - const alt = 'alt text'; - let node; - - beforeEach(() => { - node = document.createElement('img'); - node.alt = alt; - node.src = originalSrc; - }); - - it('returns an image with its original src of the `original-src` attribute is preset', () => { - node.dataset.originalSrc = originalSrc; - node.src = 'modified/path/to/image.png'; - - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - - expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`); - }); - - it('fallback to `src` if no `original-src` is specified on the image', () => { - htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js deleted file mode 100644 index 7a7e3055520..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { - buildTextToken, - buildUneditableOpenTokens, - buildUneditableCloseToken, - buildUneditableCloseTokens, - buildUneditableBlockTokens, - buildUneditableInlineTokens, - buildUneditableHtmlAsTextTokens, -} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; - -import { - originInlineToken, - originToken, - uneditableOpenTokens, - uneditableCloseToken, - uneditableCloseTokens, - uneditableBlockTokens, - uneditableInlineTokens, - uneditableTokens, -} from './mock_data'; - -describe('Build Uneditable Token renderer helper', () => { - describe('buildTextToken', () => { - it('returns an object literal representing a text token', () => { - const text = originToken.content; - expect(buildTextToken(text)).toStrictEqual(originToken); - }); - }); - - describe('buildUneditableOpenTokens', () => { - it('returns a 2-item array of tokens with the originToken appended to an open token', () => { - const result = buildUneditableOpenTokens(originToken); - - expect(result).toHaveLength(2); - expect(result).toStrictEqual(uneditableOpenTokens); - }); - }); - - describe('buildUneditableCloseToken', () => { - it('returns an object literal representing the uneditable close token', () => { - expect(buildUneditableCloseToken()).toStrictEqual(uneditableCloseToken); - }); - }); - - describe('buildUneditableCloseTokens', () => { - it('returns a 2-item array of tokens with the originToken prepended to a close token', () => { - const result = buildUneditableCloseTokens(originToken); - - expect(result).toHaveLength(2); - expect(result).toStrictEqual(uneditableCloseTokens); - }); - }); - - describe('buildUneditableBlockTokens', () => { - it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { - const result = buildUneditableBlockTokens(originToken); - - expect(result).toHaveLength(3); - expect(result).toStrictEqual(uneditableTokens); - }); - }); - - describe('buildUneditableInlineTokens', () => { - it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => { - const result = buildUneditableInlineTokens(originInlineToken); - - expect(result).toHaveLength(3); - expect(result).toStrictEqual(uneditableInlineTokens); - }); - }); - - describe('buildUneditableHtmlAsTextTokens', () => { - it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => { - const htmlBlockNode = { - type: 'htmlBlock', - literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>', - }; - const result = buildUneditableHtmlAsTextTokens(htmlBlockNode); - const { type, content } = result[1]; - - expect(type).toBe('text'); - expect(content).not.toMatch(/ data-tomark-pass /); - - expect(result).toHaveLength(3); - expect(result).toStrictEqual(uneditableBlockTokens); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js deleted file mode 100644 index 407072fb596..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js +++ /dev/null @@ -1,54 +0,0 @@ -// Node spec helpers - -export const buildMockTextNode = (literal) => ({ literal, type: 'text' }); - -export const normalTextNode = buildMockTextNode('This is just normal text.'); - -// Token spec helpers - -const buildMockUneditableOpenToken = (type) => { - return { - type: 'openTag', - tagName: type, - attributes: { contenteditable: false }, - classNames: [ - 'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', - ], - }; -}; - -const buildMockTextToken = (content) => { - return { - type: 'text', - tagName: null, - content, - }; -}; - -const buildMockUneditableCloseToken = (type) => ({ type: 'closeTag', tagName: type }); - -export const originToken = buildMockTextToken('{:.no_toc .hidden-md .hidden-lg}'); -const uneditableOpenToken = buildMockUneditableOpenToken('div'); -export const uneditableOpenTokens = [uneditableOpenToken, originToken]; -export const uneditableCloseToken = buildMockUneditableCloseToken('div'); -export const uneditableCloseTokens = [originToken, uneditableCloseToken]; -export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; - -export const originInlineToken = { - type: 'text', - content: '<i>Inline</i> content', -}; - -export const uneditableInlineTokens = [ - buildMockUneditableOpenToken('a'), - originInlineToken, - buildMockUneditableCloseToken('a'), -]; - -export const uneditableBlockTokens = [ - uneditableOpenToken, - buildMockTextToken('<div><h1>Some header</h1><p>Some paragraph</p></div>'), - uneditableCloseToken, -]; - -export const attributeDefinition = '{:.no_toc .hidden-md .hidden-lg}'; diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js deleted file mode 100644 index 69fd9a67a21..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition'; -import { attributeDefinition } from './mock_data'; - -describe('rich_content_editor/renderers/render_attribute_definition', () => { - describe('canRender', () => { - it.each` - input | result - ${{ literal: attributeDefinition }} | ${true} - ${{ literal: `FOO${attributeDefinition}` }} | ${false} - ${{ literal: `${attributeDefinition}BAR` }} | ${false} - ${{ literal: 'foobar' }} | ${false} - `('returns $result when input is $input', ({ input, result }) => { - expect(renderer.canRender(input)).toBe(result); - }); - }); - - describe('render', () => { - it('returns an empty HTML comment', () => { - expect(renderer.render()).toEqual({ - type: 'html', - content: '<!-- sse-attribute-definition -->', - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js deleted file mode 100644 index 0c59d9f569b..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text'; -import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -import { buildMockTextNode, normalTextNode } from './mock_data'; - -const embeddedRubyTextNode = buildMockTextNode('<%= partial("some/path") %>'); - -describe('Render Embedded Ruby Text renderer', () => { - describe('canRender', () => { - it('should return true when the argument `literal` has embedded ruby syntax', () => { - expect(renderer.canRender(embeddedRubyTextNode)).toBe(true); - }); - - it('should return false when the argument `literal` lacks embedded ruby syntax', () => { - expect(renderer.canRender(normalTextNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should delegate rendering to the renderUneditableLeaf util', () => { - expect(renderer.render).toBe(renderUneditableLeaf); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js deleted file mode 100644 index c1aaed6f0c3..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline'; - -import { normalTextNode } from './mock_data'; - -const fontAwesomeInlineHtmlNode = { - firstChild: null, - literal: '<i class="far fa-paper-plane" id="biz-tech-icons">', - type: 'html', -}; - -describe('Render Font Awesome Inline HTML renderer', () => { - describe('canRender', () => { - it('should return true when the argument `literal` has font awesome inline html syntax', () => { - expect(renderer.canRender(fontAwesomeInlineHtmlNode)).toBe(true); - }); - - it('should return false when the argument `literal` lacks font awesome inline html syntax', () => { - expect(renderer.canRender(normalTextNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should return uneditable inline tokens', () => { - const token = { type: 'text', tagName: null, content: fontAwesomeInlineHtmlNode.literal }; - const context = { origin: () => token }; - - expect(renderer.render(fontAwesomeInlineHtmlNode, context)).toStrictEqual( - buildUneditableInlineTokens(token), - ); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js deleted file mode 100644 index 76abc1ec3d8..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js +++ /dev/null @@ -1,12 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_heading'; -import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -describe('rich_content_editor/renderers/render_heading', () => { - it('canRender delegates to renderUtils.willAlwaysRender', () => { - expect(renderer.canRender).toBe(renderUtils.willAlwaysRender); - }); - - it('render delegates to renderUtils.renderWithAttributeDefinitions', () => { - expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js deleted file mode 100644 index 234f6a4d4ca..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block'; - -describe('rich_content_editor/services/renderers/render_html_block', () => { - const htmlBlockNode = { - literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>', - type: 'htmlBlock', - }; - - describe('canRender', () => { - it.each` - input | result - ${htmlBlockNode} | ${true} - ${{ literal: '<iframe></iframe>', type: 'htmlBlock' }} | ${true} - ${{ literal: '<iframe src="https://www.youtube.com"></iframe>', type: 'htmlBlock' }} | ${false} - ${{ literal: '<iframe></iframe>', type: 'text' }} | ${false} - `('returns $result when input=$input', ({ input, result }) => { - expect(renderer.canRender(input)).toBe(result); - }); - }); - - describe('render', () => { - const htmlBlockNodeToMark = { - firstChild: null, - literal: '<div data-to-mark ></div>', - type: 'htmlBlock', - }; - - it.each` - node - ${htmlBlockNode} - ${htmlBlockNodeToMark} - `('should return uneditable tokens wrapping the $node as a token', ({ node }) => { - expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node)); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js deleted file mode 100644 index 425d0f41bcd..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text'; - -import { buildMockTextNode, normalTextNode } from './mock_data'; - -const mockTextStart = 'Majority example '; -const mockTextMiddle = '[environment terraform plans][terraform]'; -const mockTextEnd = '.'; -const identifierInstanceStartTextNode = buildMockTextNode(mockTextStart); -const identifierInstanceEndTextNode = buildMockTextNode(mockTextEnd); - -describe('Render Identifier Instance Text renderer', () => { - describe('canRender', () => { - it.each` - node | target - ${normalTextNode} | ${false} - ${identifierInstanceStartTextNode} | ${false} - ${identifierInstanceEndTextNode} | ${false} - ${buildMockTextNode(mockTextMiddle)} | ${true} - ${buildMockTextNode('Minority example [environment terraform plans][]')} | ${true} - ${buildMockTextNode('Minority example [environment terraform plans]')} | ${true} - `( - 'should return $target when the $node validates against identifier instance syntax', - ({ node, target }) => { - expect(renderer.canRender(node)).toBe(target); - }, - ); - }); - - describe('render', () => { - it.each` - start | middle | end - ${mockTextStart} | ${mockTextMiddle} | ${mockTextEnd} - ${mockTextStart} | ${'[environment terraform plans][]'} | ${mockTextEnd} - ${mockTextStart} | ${'[environment terraform plans]'} | ${mockTextEnd} - `( - 'should return inline editable, uneditable, and editable tokens in sequence', - ({ start, middle, end }) => { - const buildMockTextToken = (content) => ({ type: 'text', tagName: null, content }); - - const startToken = buildMockTextToken(start); - const middleToken = buildMockTextToken(middle); - const endToken = buildMockTextToken(end); - - const content = `${start}${middle}${end}`; - const contentToken = buildMockTextToken(content); - const contentNode = buildMockTextNode(content); - const context = { origin: jest.fn().mockReturnValueOnce(contentToken) }; - expect(renderer.render(contentNode, context)).toStrictEqual( - [startToken, buildUneditableInlineTokens(middleToken), endToken].flat(), - ); - }, - ); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js deleted file mode 100644 index 470cf9bddaa..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph'; - -import { buildMockTextNode } from './mock_data'; - -const buildMockParagraphNode = (literal) => { - return { - firstChild: buildMockTextNode(literal), - type: 'paragraph', - }; -}; - -const normalParagraphNode = buildMockParagraphNode( - 'This is just normal paragraph. It has multiple sentences.', -); -const identifierParagraphNode = buildMockParagraphNode( - `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`, -); - -describe('rich_content_editor/renderers_render_identifier_paragraph', () => { - describe('canRender', () => { - it.each` - node | paragraph | target - ${identifierParagraphNode} | ${'[Some text]: https://link.com'} | ${true} - ${normalParagraphNode} | ${'Normal non-identifier text. Another sentence.'} | ${false} - `( - 'should return $target when the $node matches $paragraph syntax', - ({ node, paragraph, target }) => { - const context = { - entering: true, - getChildrenText: jest.fn().mockReturnValueOnce(paragraph), - }; - - expect(renderer.canRender(node, context)).toBe(target); - }, - ); - }); - - describe('render', () => { - let context; - let result; - - beforeEach(() => { - const node = { - firstChild: { - type: 'text', - literal: '[Some text]: https://link.com', - next: { - type: 'linebreak', - next: { - type: 'text', - literal: '[identifier]: http://example1.com "title"', - }, - }, - }, - }; - context = { skipChildren: jest.fn() }; - result = renderer.render(node, context); - }); - - it('renders the reference definitions as a code block', () => { - expect(result).toEqual([ - { - type: 'openTag', - tagName: 'pre', - classNames: ['code-block', 'language-markdown'], - attributes: { - 'data-sse-reference-definition': true, - }, - }, - { type: 'openTag', tagName: 'code' }, - { - type: 'text', - content: '[Some text]: https://link.com\n[identifier]: http://example1.com "title"', - }, - { type: 'closeTag', tagName: 'code' }, - { type: 'closeTag', tagName: 'pre' }, - ]); - }); - - it('skips the reference definition node children from rendering', () => { - expect(context.skipChildren).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js deleted file mode 100644 index c1ab700535b..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js +++ /dev/null @@ -1,12 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_list_item'; -import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -describe('rich_content_editor/renderers/render_list_item', () => { - it('canRender delegates to renderUtils.willAlwaysRender', () => { - expect(renderer.canRender).toBe(renderUtils.willAlwaysRender); - }); - - it('render delegates to renderUtils.renderWithAttributeDefinitions', () => { - expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js deleted file mode 100644 index 3c3d2354cb9..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_softbreak'; - -describe('Render softbreak renderer', () => { - describe('canRender', () => { - it.each` - node | parentType | result - ${{ parent: { type: 'emph' } }} | ${'emph'} | ${true} - ${{ parent: { type: 'strong' } }} | ${'strong'} | ${true} - ${{ parent: { type: 'paragraph' } }} | ${'paragraph'} | ${false} - `('returns $result when node parent type is $parentType ', ({ node, result }) => { - expect(renderer.canRender(node)).toBe(result); - }); - }); - - describe('render', () => { - it('returns text node with a break line', () => { - expect(renderer.render()).toEqual({ - type: 'text', - content: ' ', - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js deleted file mode 100644 index 7c1809c290c..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import { - buildUneditableBlockTokens, - buildUneditableOpenTokens, -} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import { - renderUneditableLeaf, - renderUneditableBranch, - renderWithAttributeDefinitions, - willAlwaysRender, -} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data'; - -describe('rich_content_editor/renderers/render_utils', () => { - describe('renderUneditableLeaf', () => { - it('should return uneditable block tokens around an origin token', () => { - const context = { origin: jest.fn().mockReturnValueOnce(originToken) }; - const result = renderUneditableLeaf({}, context); - - expect(result).toStrictEqual(buildUneditableBlockTokens(originToken)); - }); - }); - - describe('renderUneditableBranch', () => { - let origin; - - beforeEach(() => { - origin = jest.fn().mockReturnValueOnce(originToken); - }); - - it('should return uneditable block open token followed by the origin token when entering', () => { - const context = { entering: true, origin }; - const result = renderUneditableBranch({}, context); - - expect(result).toStrictEqual(buildUneditableOpenTokens(originToken)); - }); - - it('should return uneditable block closing token when exiting', () => { - const context = { entering: false, origin }; - const result = renderUneditableBranch({}, context); - - expect(result).toStrictEqual(uneditableCloseToken); - }); - }); - - describe('willAlwaysRender', () => { - it('always returns true', () => { - expect(willAlwaysRender()).toBe(true); - }); - }); - - describe('renderWithAttributeDefinitions', () => { - let openTagToken; - let closeTagToken; - let node; - const attributes = { - 'data-attribute-definition': attributeDefinition, - }; - - beforeEach(() => { - openTagToken = { type: 'openTag' }; - closeTagToken = { type: 'closeTag' }; - node = { - next: { - firstChild: { - literal: attributeDefinition, - }, - }, - }; - }); - - describe('when token type is openTag', () => { - it('attaches attributes when attributes exist in the node’s next sibling', () => { - const context = { origin: () => openTagToken }; - - expect(renderWithAttributeDefinitions(node, context)).toEqual({ - ...openTagToken, - attributes, - }); - }); - - it('attaches attributes when attributes exist in the node’s children', () => { - const context = { origin: () => openTagToken }; - node = { - firstChild: { - firstChild: { - next: { - next: { - literal: attributeDefinition, - }, - }, - }, - }, - }; - - expect(renderWithAttributeDefinitions(node, context)).toEqual({ - ...openTagToken, - attributes, - }); - }); - }); - - it('does not attach attributes when token type is "closeTag"', () => { - const context = { origin: () => closeTagToken }; - - expect(renderWithAttributeDefinitions({}, context)).toBe(closeTagToken); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/sanitize_html_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/sanitize_html_spec.js deleted file mode 100644 index f2182ef60d7..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/sanitize_html_spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html'; - -describe('rich_content_editor/services/sanitize_html', () => { - it.each` - input | result - ${'<iframe src="https://www.youtube.com"></iframe>'} | ${'<iframe src="https://www.youtube.com"></iframe>'} - ${'<iframe src="https://gitlab.com"></iframe>'} | ${''} - `('removes iframes if the iframe source origin is not allowed', ({ input, result }) => { - expect(sanitizeHTML(input)).toBe(result); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js deleted file mode 100644 index 5a56b499769..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import ToolbarItem from '~/vue_shared/components/rich_content_editor/toolbar_item.vue'; - -describe('Toolbar Item', () => { - let wrapper; - - const findIcon = () => wrapper.find(GlIcon); - const findButton = () => wrapper.find('button'); - - const buildWrapper = (propsData) => { - wrapper = shallowMount(ToolbarItem, { - propsData, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - describe.each` - icon | tooltip - ${'heading'} | ${'Headings'} - ${'bold'} | ${'Add bold text'} - ${'italic'} | ${'Add italic text'} - ${'strikethrough'} | ${'Add strikethrough text'} - ${'quote'} | ${'Insert a quote'} - ${'link'} | ${'Add a link'} - ${'doc-code'} | ${'Insert a code block'} - ${'list-bulleted'} | ${'Add a bullet list'} - ${'list-numbered'} | ${'Add a numbered list'} - ${'list-task'} | ${'Add a task list'} - ${'list-indent'} | ${'Indent'} - ${'list-outdent'} | ${'Outdent'} - ${'dash'} | ${'Add a line'} - ${'table'} | ${'Add a table'} - ${'code'} | ${'Insert an image'} - ${'code'} | ${'Insert inline code'} - `('toolbar item component', ({ icon, tooltip }) => { - beforeEach(() => buildWrapper({ icon, tooltip })); - - it('renders a toolbar button', () => { - expect(findButton().exists()).toBe(true); - }); - - it('renders the correct tooltip', () => { - const buttonTooltip = getBinding(wrapper.element, 'gl-tooltip'); - expect(buttonTooltip).toBeDefined(); - expect(buttonTooltip.value.title).toBe(tooltip); - }); - - it(`renders the ${icon} icon`, () => { - expect(findIcon().exists()).toBe(true); - expect(findIcon().props().name).toBe(icon); - }); - }); -}); |