diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/frontend/vue_shared/components/rich_content_editor/services | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) | |
download | gitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared/components/rich_content_editor/services')
11 files changed, 531 insertions, 0 deletions
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 new file mode 100644 index 00000000000..cafe53e6bb2 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js @@ -0,0 +1,29 @@ +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({ + list: expect.any(Function), + text: 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), + list: expect.any(Function), + text: 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 new file mode 100644 index 00000000000..0e8610a22f5 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js @@ -0,0 +1,50 @@ +import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; + +describe('HTMLToMarkdownRenderer', () => { + let baseRenderer; + let htmlToMarkdownRenderer; + const NODE = { nodeValue: 'mock_node' }; + + 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(), + }; + }); + + describe('TEXT_NODE visitor', () => { + it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => { + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); + + expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe( + `space controlled trimmed space collapsed ${NODE.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'](NODE, list)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list); + }); + }); +}); 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 new file mode 100644 index 00000000000..18dff0a39bb --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js @@ -0,0 +1,88 @@ +import { + buildTextToken, + buildUneditableOpenTokens, + buildUneditableCloseToken, + buildUneditableCloseTokens, + buildUneditableTokens, + 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('buildUneditableTokens', () => { + it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { + const result = buildUneditableTokens(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 new file mode 100644 index 00000000000..660c21281fd --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js @@ -0,0 +1,58 @@ +// Node spec helpers + +export const buildMockTextNode = literal => { + return { + firstChild: null, + 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 buildMockUneditableCloseToken = type => { + return { type: 'closeTag', tagName: type }; +}; + +export const originToken = { + type: 'text', + tagName: null, + content: '{:.no_toc .hidden-md .hidden-lg}', +}; +export const uneditableCloseToken = buildMockUneditableCloseToken('div'); +export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken]; +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 = [ + buildMockUneditableOpenToken('div'), + { + type: 'text', + tagName: null, + content: '<div><h1>Some header</h1><p>Some paragraph</p></div>', + }, + buildMockUneditableCloseToken('div'), +]; 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 new file mode 100644 index 00000000000..b723ee8c8a0 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js @@ -0,0 +1,30 @@ +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 { 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', () => { + const origin = jest.fn(); + + it('should return uneditable tokens', () => { + const context = { origin }; + + expect(renderer.render(embeddedRubyTextNode, context)).toStrictEqual( + buildUneditableTokens(origin()), + ); + }); + }); +}); 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 new file mode 100644 index 00000000000..d6bb01259bb --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js @@ -0,0 +1,33 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline'; +import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; + +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_html_block_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js new file mode 100644 index 00000000000..a6c712eeb31 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js @@ -0,0 +1,38 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block'; +import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; + +import { normalTextNode } from './mock_data'; + +const htmlBlockNode = { + firstChild: null, + literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>', + type: 'htmlBlock', +}; + +describe('Render HTML renderer', () => { + describe('canRender', () => { + it('should return true when the argument is an html block', () => { + expect(renderer.canRender(htmlBlockNode)).toBe(true); + }); + + it('should return false when the argument is not an html block', () => { + expect(renderer.canRender(normalTextNode)).toBe(false); + }); + }); + + 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 new file mode 100644 index 00000000000..2897929f1bf --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js @@ -0,0 +1,55 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text'; +import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; + +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 new file mode 100644 index 00000000000..320589e4de3 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js @@ -0,0 +1,65 @@ +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 { 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('Render Identifier Paragraph renderer', () => { + 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 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()), + ); + }); + }); +}); 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 new file mode 100644 index 00000000000..e60bf1c8c92 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js @@ -0,0 +1,55 @@ +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 { buildMockTextNode } from './mock_data'; + +const buildMockListNode = literal => { + return { + firstChild: { + firstChild: { + firstChild: buildMockTextNode(literal), + type: 'paragraph', + }, + type: 'item', + }, + type: 'list', + }; +}; + +const normalListNode = buildMockListNode('Just another bullet point'); +const kramdownListNode = buildMockListNode('TOC'); + +describe('Render Kramdown List renderer', () => { + describe('canRender', () => { + it('should return true when the argument is a special kramdown TOC ordered/unordered list', () => { + expect(renderer.canRender(kramdownListNode)).toBe(true); + }); + + it('should return false when the argument is a normal ordered/unordered list', () => { + expect(renderer.canRender(normalListNode)).toBe(false); + }); + }); + + 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()), + ); + }); + }); +}); 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 new file mode 100644 index 00000000000..97ff9794e69 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js @@ -0,0 +1,30 @@ +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 { buildMockTextNode, normalTextNode } from './mock_data'; + +const kramdownTextNode = buildMockTextNode('{:toc}'); + +describe('Render Kramdown Text renderer', () => { + describe('canRender', () => { + it('should return true when the argument `literal` has kramdown syntax', () => { + expect(renderer.canRender(kramdownTextNode)).toBe(true); + }); + + it('should return false when the argument `literal` lacks kramdown syntax', () => { + expect(renderer.canRender(normalTextNode)).toBe(false); + }); + }); + + describe('render', () => { + const origin = jest.fn(); + + it('should return uneditable tokens', () => { + const context = { origin }; + + expect(renderer.render(kramdownTextNode, context)).toStrictEqual( + buildUneditableTokens(origin()), + ); + }); + }); +}); |