diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/frontend/vue_shared/components/rich_content_editor/services | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) | |
download | gitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/frontend/vue_shared/components/rich_content_editor/services')
10 files changed, 258 insertions, 107 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 index cafe53e6bb2..a823d04024d 100644 --- 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 @@ -5,8 +5,13 @@ describe('Build Custom Renderer Service', () => { it('should return an object with the default renderer functions when lacking arguments', () => { expect(buildCustomHTMLRenderer()).toEqual( expect.objectContaining({ - list: expect.any(Function), + 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), }), ); }); @@ -20,8 +25,6 @@ describe('Build Custom Renderer Service', () => { 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 index a90d3528d60..fd745c21bb6 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 @@ -1,9 +1,10 @@ import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; +import { attributeDefinition } from './renderers/mock_data'; -describe('HTMLToMarkdownRenderer', () => { +describe('rich_content_editor/services/html_to_markdown_renderer', () => { let baseRenderer; let htmlToMarkdownRenderer; - const NODE = { nodeValue: 'mock_node' }; + let fakeNode; beforeEach(() => { baseRenderer = { @@ -12,14 +13,20 @@ describe('HTMLToMarkdownRenderer', () => { 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(NODE)).toBe( - `space controlled trimmed space collapsed ${NODE.nodeValue}`, + expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe( + `space controlled trimmed space collapsed ${fakeNode.nodeValue}`, ); }); }); @@ -43,8 +50,8 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(list); - expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list); + expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list); }); }); @@ -62,10 +69,21 @@ describe('HTMLToMarkdownRenderer', () => { }); baseRenderer.convert.mockReturnValueOnce(listItem); - expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, 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', () => { @@ -85,8 +103,8 @@ describe('HTMLToMarkdownRenderer', () => { }); baseRenderer.convert.mockReturnValueOnce(listItem); - expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent); + expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent); }, ); }); @@ -105,8 +123,8 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(input); - expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); }, ); }); @@ -125,9 +143,50 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(input); - expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, 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); + }); + }); }); 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 index 660c21281fd..5cf3961819e 100644 --- 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 @@ -1,12 +1,6 @@ // Node spec helpers -export const buildMockTextNode = literal => { - return { - firstChild: null, - literal, - type: 'text', - }; -}; +export const buildMockTextNode = literal => ({ literal, type: 'text' }); export const normalTextNode = buildMockTextNode('This is just normal text.'); @@ -23,17 +17,20 @@ const buildMockUneditableOpenToken = type => { }; }; -const buildMockUneditableCloseToken = type => { - return { type: 'closeTag', tagName: type }; +const buildMockTextToken = content => { + return { + type: 'text', + tagName: null, + content, + }; }; -export const originToken = { - type: 'text', - tagName: null, - content: '{:.no_toc .hidden-md .hidden-lg}', -}; +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 uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken]; export const uneditableCloseTokens = [originToken, uneditableCloseToken]; export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; @@ -41,6 +38,7 @@ export const originInlineToken = { type: 'text', content: '<i>Inline</i> content', }; + export const uneditableInlineTokens = [ buildMockUneditableOpenToken('a'), originInlineToken, @@ -48,11 +46,9 @@ export const uneditableInlineTokens = [ ]; export const uneditableBlockTokens = [ - buildMockUneditableOpenToken('div'), - { - type: 'text', - tagName: null, - content: '<div><h1>Some header</h1><p>Some paragraph</p></div>', - }, - buildMockUneditableCloseToken('div'), + 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 new file mode 100644 index 00000000000..69fd9a67a21 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js @@ -0,0 +1,25 @@ +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_heading_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js new file mode 100644 index 00000000000..76abc1ec3d8 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js @@ -0,0 +1,12 @@ +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_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js index f4a06b91a10..b3d9576f38b 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,5 +1,4 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph'; -import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode } from './mock_data'; @@ -17,7 +16,7 @@ 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('rich_content_editor/renderers_render_identifier_paragraph', () => { describe('canRender', () => { it.each` node | paragraph | target @@ -37,8 +36,49 @@ describe('Render Identifier Paragraph renderer', () => { }); describe('render', () => { - it('should delegate rendering to the renderUneditableBranch util', () => { - expect(renderer.render).toBe(renderUneditableBranch); + 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_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js deleted file mode 100644 index 7d427108ba6..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list'; -import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -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', () => { - 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 deleted file mode 100644 index 1d2d152ffc3..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text'; -import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -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', () => { - 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_list_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js new file mode 100644 index 00000000000..c1ab700535b --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js @@ -0,0 +1,12 @@ +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_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js index 92435b3e4e3..774f830f421 100644 --- 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 @@ -1,6 +1,8 @@ import { renderUneditableLeaf, renderUneditableBranch, + renderWithAttributeDefinitions, + willAlwaysRender, } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { @@ -8,9 +10,9 @@ import { buildUneditableOpenTokens, } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import { originToken, uneditableCloseToken } from './mock_data'; +import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data'; -describe('Render utils', () => { +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) }; @@ -41,4 +43,68 @@ describe('Render utils', () => { 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); + }); + }); }); |