diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/rich_content_editor')
11 files changed, 295 insertions, 79 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 index 78f27c9948b..16f60b5ff21 100644 --- 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 @@ -5,10 +5,13 @@ import { registerHTMLToMarkdownRenderer, addImage, getMarkdown, + getEditorOptions, } from '~/vue_shared/components/rich_content_editor/services/editor_service'; import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; +import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; 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'); describe('Editor Service', () => { let mockInstance; @@ -120,4 +123,25 @@ describe('Editor Service', () => { 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); + }); + }); }); 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 new file mode 100644 index 00000000000..b9b93b274d2 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js @@ -0,0 +1,69 @@ +import Editor from '@toast-ui/editor'; +import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service'; +import buildMarkdownToHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; + +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 index b6ff6aa767c..3d54db7fe5c 100644 --- 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 @@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils'; import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; import { - EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, @@ -14,6 +13,7 @@ import { removeCustomEventListener, addImage, registerHTMLToMarkdownRenderer, + getEditorOptions, } from '~/vue_shared/components/rich_content_editor/services/editor_service'; jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({ @@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', removeCustomEventListener: jest.fn(), addImage: jest.fn(), registerHTMLToMarkdownRenderer: jest.fn(), + getEditorOptions: jest.fn(), })); describe('Rich Content Editor', () => { @@ -32,13 +33,25 @@ describe('Rich Content Editor', () => { const findEditor = () => wrapper.find({ ref: 'editor' }); const findAddImageModal = () => wrapper.find(AddImageModal); - beforeEach(() => { + const buildWrapper = () => { wrapper = shallowMount(RichContentEditor, { propsData: { content, imageRoot }, }); + }; + + 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); }); @@ -47,8 +60,8 @@ describe('Rich Content Editor', () => { expect(findEditor().props().initialValue).toBe(content); }); - it('provides the correct editor options', () => { - expect(findEditor().props().options).toEqual(EDITOR_OPTIONS); + it('provides options generated by the getEditorOptions service', () => { + expect(findEditor().props().options).toBe(editorOptions); }); it('has the correct preview style', () => { @@ -65,6 +78,10 @@ describe('Rich Content Editor', () => { }); describe('when content is changed', () => { + beforeEach(() => { + buildWrapper(); + }); + it('emits an input event with the changed content', () => { const changedMarkdown = '## Changed Markdown'; const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown); @@ -77,6 +94,10 @@ describe('Rich Content Editor', () => { }); 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() }; @@ -89,35 +110,33 @@ describe('Rich Content Editor', () => { }); describe('when editor is loaded', () => { - let mockEditorApi; - beforeEach(() => { - mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } }; - findEditor().vm.$emit('load', mockEditorApi); + buildWrapper(); }); it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { expect(addCustomEventListener).toHaveBeenCalledWith( - mockEditorApi, + wrapper.vm.editorApi, CUSTOM_EVENTS.openAddImageModal, wrapper.vm.onOpenAddImageModal, ); }); it('registers HTML to markdown renderer', () => { - expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi); + expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi); }); }); describe('when editor is destroyed', () => { - it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { - const mockEditorApi = { eventManager: { removeEventHandler: jest.fn() } }; + beforeEach(() => { + buildWrapper(); + }); - wrapper.vm.editorApi = mockEditorApi; + it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { wrapper.vm.$destroy(); expect(removeCustomEventListener).toHaveBeenCalledWith( - mockEditorApi, + wrapper.vm.editorApi, CUSTOM_EVENTS.openAddImageModal, wrapper.vm.onOpenAddImageModal, ); @@ -125,6 +144,10 @@ describe('Rich Content Editor', () => { }); describe('add image modal', () => { + beforeEach(() => { + buildWrapper(); + }); + it('renders an addImageModal component', () => { expect(findAddImageModal().exists()).toBe(true); }); 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 index 0e8610a22f5..a90d3528d60 100644 --- 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 @@ -47,4 +47,87 @@ describe('HTMLToMarkdownRenderer', () => { expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, 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'](NODE, listItem)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem); + }, + ); + }); + + 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'](NODE, subContent)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, 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'](NODE, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, 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'](NODE, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + }, + ); + }); }); 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 index 18dff0a39bb..7a7e3055520 100644 --- 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 @@ -3,7 +3,7 @@ import { buildUneditableOpenTokens, buildUneditableCloseToken, buildUneditableCloseTokens, - buildUneditableTokens, + buildUneditableBlockTokens, buildUneditableInlineTokens, buildUneditableHtmlAsTextTokens, } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; @@ -51,9 +51,9 @@ describe('Build Uneditable Token renderer helper', () => { }); }); - describe('buildUneditableTokens', () => { + describe('buildUneditableBlockTokens', () => { it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { - const result = buildUneditableTokens(originToken); + const result = buildUneditableBlockTokens(originToken); expect(result).toHaveLength(3); expect(result).toStrictEqual(uneditableTokens); 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 index b723ee8c8a0..0c59d9f569b 100644 --- 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 @@ -1,5 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text'; -import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode, normalTextNode } from './mock_data'; @@ -17,14 +17,8 @@ describe('Render Embedded Ruby Text renderer', () => { }); describe('render', () => { - const origin = jest.fn(); - - it('should return uneditable tokens', () => { - const context = { origin }; - - expect(renderer.render(embeddedRubyTextNode, context)).toStrictEqual( - buildUneditableTokens(origin()), - ); + 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_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js index 320589e4de3..f4a06b91a10 100644 --- 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 @@ -1,8 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph'; -import { - buildUneditableOpenTokens, - buildUneditableCloseToken, -} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode } from './mock_data'; @@ -40,26 +37,8 @@ describe('Render Identifier Paragraph renderer', () => { }); describe('render', () => { - let origin; - - beforeEach(() => { - origin = jest.fn(); - }); - - it('should return uneditable open tokens when entering', () => { - const context = { entering: true, origin }; - - expect(renderer.render(identifierParagraphNode, context)).toStrictEqual( - buildUneditableOpenTokens(origin()), - ); - }); - - it('should return an uneditable close tokens when exiting', () => { - const context = { entering: false, origin }; - - expect(renderer.render(identifierParagraphNode, context)).toStrictEqual( - buildUneditableCloseToken(origin()), - ); + it('should delegate rendering to the renderUneditableBranch util', () => { + expect(renderer.render).toBe(renderUneditableBranch); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js index e60bf1c8c92..7d427108ba6 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js @@ -1,8 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list'; -import { - buildUneditableOpenTokens, - buildUneditableCloseToken, -} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode } from './mock_data'; @@ -34,22 +31,8 @@ describe('Render Kramdown List renderer', () => { }); describe('render', () => { - const origin = jest.fn(); - - it('should return uneditable open tokens when entering', () => { - const context = { entering: true, origin }; - - expect(renderer.render(kramdownListNode, context)).toStrictEqual( - buildUneditableOpenTokens(origin()), - ); - }); - - it('should return an uneditable close tokens when exiting', () => { - const context = { entering: false, origin }; - - expect(renderer.render(kramdownListNode, context)).toStrictEqual( - buildUneditableCloseToken(origin()), - ); + it('should delegate rendering to the renderUneditableBranch util', () => { + expect(renderer.render).toBe(renderUneditableBranch); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js index 97ff9794e69..1d2d152ffc3 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js @@ -1,5 +1,5 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text'; -import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; +import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode, normalTextNode } from './mock_data'; @@ -17,14 +17,8 @@ describe('Render Kramdown Text renderer', () => { }); describe('render', () => { - const origin = jest.fn(); - - it('should return uneditable tokens', () => { - const context = { origin }; - - expect(renderer.render(kramdownTextNode, context)).toStrictEqual( - buildUneditableTokens(origin()), - ); + 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_softbreak_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js new file mode 100644 index 00000000000..3c3d2354cb9 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000000..92435b3e4e3 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js @@ -0,0 +1,44 @@ +import { + renderUneditableLeaf, + renderUneditableBranch, +} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; + +import { + buildUneditableBlockTokens, + buildUneditableOpenTokens, +} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; + +import { originToken, uneditableCloseToken } from './mock_data'; + +describe('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); + }); + }); +}); |