diff options
Diffstat (limited to 'spec/frontend/content_editor/services')
-rw-r--r-- | spec/frontend/content_editor/services/asset_resolver_spec.js | 23 | ||||
-rw-r--r-- | spec/frontend/content_editor/services/code_block_language_loader_spec.js | 36 | ||||
-rw-r--r-- | spec/frontend/content_editor/services/content_editor_spec.js | 15 | ||||
-rw-r--r-- | spec/frontend/content_editor/services/create_content_editor_spec.js | 48 | ||||
-rw-r--r-- | spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js (renamed from spec/frontend/content_editor/services/markdown_deserializer_spec.js) | 16 | ||||
-rw-r--r-- | spec/frontend/content_editor/services/markdown_serializer_spec.js | 44 | ||||
-rw-r--r-- | spec/frontend/content_editor/services/markdown_sourcemap_spec.js | 2 |
7 files changed, 117 insertions, 67 deletions
diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js new file mode 100644 index 00000000000..f4e7d9bf881 --- /dev/null +++ b/spec/frontend/content_editor/services/asset_resolver_spec.js @@ -0,0 +1,23 @@ +import createAssetResolver from '~/content_editor/services/asset_resolver'; + +describe('content_editor/services/asset_resolver', () => { + let renderMarkdown; + let assetResolver; + + beforeEach(() => { + renderMarkdown = jest.fn(); + assetResolver = createAssetResolver({ renderMarkdown }); + }); + + describe('resolveUrl', () => { + it('resolves a canonical url to an absolute url', async () => { + renderMarkdown.mockResolvedValue( + '<p><a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">link</a></p>', + ); + + expect(await assetResolver.resolveUrl('test-file.png')).toBe( + '/group1/project1/-/wikis/test-file.png', + ); + }); + }); +}); diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js index 905c1685b94..943de327762 100644 --- a/spec/frontend/content_editor/services/code_block_language_loader_spec.js +++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js @@ -53,47 +53,25 @@ describe('content_editor/services/code_block_language_loader', () => { }); }); - describe('loadLanguages', () => { + describe('loadLanguage', () => { it('loads highlight.js language packages identified by a list of languages', async () => { - const languages = ['javascript', 'ruby']; + const language = 'javascript'; - await languageLoader.loadLanguages(languages); + await languageLoader.loadLanguage(language); - languages.forEach((language) => { - expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function)); - }); + expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function)); }); describe('when language is already registered', () => { it('does not load the language again', async () => { - const languages = ['javascript']; - - await languageLoader.loadLanguages(languages); - await languageLoader.loadLanguages(languages); + await languageLoader.loadLanguage('javascript'); + await languageLoader.loadLanguage('javascript'); expect(lowlight.registerLanguage).toHaveBeenCalledTimes(1); }); }); }); - describe('loadLanguagesFromDOM', () => { - it('loads highlight.js language packages identified by pre tags in a DOM fragment', async () => { - const parser = new DOMParser(); - const { body } = parser.parseFromString( - ` - <pre lang="javascript"></pre> - <pre lang="ruby"></pre> - `, - 'text/html', - ); - - await languageLoader.loadLanguagesFromDOM(body); - - expect(lowlight.registerLanguage).toHaveBeenCalledWith('javascript', expect.any(Function)); - expect(lowlight.registerLanguage).toHaveBeenCalledWith('ruby', expect.any(Function)); - }); - }); - describe('loadLanguageFromInputRule', () => { it('loads highlight.js language packages identified from the input rule', async () => { const match = new RegExp(backtickInputRegex).exec('```js '); @@ -112,7 +90,7 @@ describe('content_editor/services/code_block_language_loader', () => { expect(languageLoader.isLanguageLoaded(language)).toBe(false); - await languageLoader.loadLanguages([language]); + await languageLoader.loadLanguage(language); expect(languageLoader.isLanguageLoaded(language)).toBe(true); }); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index 5b7a27b501d..a3553e612ca 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -11,7 +11,6 @@ describe('content_editor/services/content_editor', () => { let contentEditor; let serializer; let deserializer; - let languageLoader; let eventHub; let doc; let p; @@ -26,16 +25,14 @@ describe('content_editor/services/content_editor', () => { tiptapEditor, })); - serializer = { deserialize: jest.fn() }; + serializer = { serialize: jest.fn() }; deserializer = { deserialize: jest.fn() }; - languageLoader = { loadLanguagesFromDOM: jest.fn() }; eventHub = eventHubFactory(); contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub, - languageLoader, }); }); @@ -51,12 +48,12 @@ describe('content_editor/services/content_editor', () => { describe('when setSerializedContent succeeds', () => { let document; - const dom = {}; + const languages = ['javascript']; const testMarkdown = '**bold text**'; beforeEach(() => { document = doc(p('document')); - deserializer.deserialize.mockResolvedValueOnce({ document, dom }); + deserializer.deserialize.mockResolvedValueOnce({ document, languages }); }); it('emits loadingContent and loadingSuccess event in the eventHub', () => { @@ -77,12 +74,6 @@ describe('content_editor/services/content_editor', () => { expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON()); }); - - it('passes deserialized DOM document to language loader', async () => { - await contentEditor.setSerializedContent(testMarkdown); - - expect(languageLoader.loadLanguagesFromDOM).toHaveBeenCalledWith(dom); - }); }); describe('when setSerializedContent fails', () => { diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js index 6b2f28b3306..e1a30819ac8 100644 --- a/spec/frontend/content_editor/services/create_content_editor_spec.js +++ b/spec/frontend/content_editor/services/create_content_editor_spec.js @@ -1,8 +1,12 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants'; import { createContentEditor } from '~/content_editor/services/create_content_editor'; +import createGlApiMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer'; +import createRemarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTestContentEditorExtension } from '../test_utils'; jest.mock('~/emoji'); +jest.mock('~/content_editor/services/remark_markdown_deserializer'); +jest.mock('~/content_editor/services/gl_api_markdown_deserializer'); describe('content_editor/services/create_content_editor', () => { let renderMarkdown; @@ -11,9 +15,36 @@ describe('content_editor/services/create_content_editor', () => { beforeEach(() => { renderMarkdown = jest.fn(); + window.gon = { + features: { + preserveUnchangedMarkdown: false, + }, + }; editor = createContentEditor({ renderMarkdown, uploadsPath }); }); + describe('when preserveUnchangedMarkdown feature is on', () => { + beforeEach(() => { + window.gon.features.preserveUnchangedMarkdown = true; + }); + + it('provides a remark markdown deserializer to the content editor class', () => { + createContentEditor({ renderMarkdown, uploadsPath }); + expect(createRemarkMarkdownDeserializer).toHaveBeenCalled(); + }); + }); + + describe('when preserveUnchangedMarkdown feature is off', () => { + beforeEach(() => { + window.gon.features.preserveUnchangedMarkdown = false; + }); + + it('provides a gl api markdown deserializer to the content editor class', () => { + createContentEditor({ renderMarkdown, uploadsPath }); + expect(createGlApiMarkdownDeserializer).toHaveBeenCalledWith({ render: renderMarkdown }); + }); + }); + it('sets gl-outline-0! class selector to the tiptapEditor instance', () => { expect(editor.tiptapEditor.options.editorProps).toMatchObject({ attributes: { @@ -22,30 +53,19 @@ describe('content_editor/services/create_content_editor', () => { }); }); - it('provides the renderMarkdown function to the markdown serializer', async () => { - const serializedContent = '**bold text**'; - - renderMarkdown.mockReturnValueOnce('<p><b>bold text</b></p>'); - - await editor.setSerializedContent(serializedContent); - - expect(renderMarkdown).toHaveBeenCalledWith(serializedContent); - }); - it('allows providing external content editor extensions', async () => { const labelReference = 'this is a ~group::editor'; const { tiptapExtension, serializer } = createTestContentEditorExtension(); - renderMarkdown.mockReturnValueOnce( - '<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>', - ); editor = createContentEditor({ renderMarkdown, extensions: [tiptapExtension], serializerConfig: { nodes: { [tiptapExtension.name]: serializer } }, }); - await editor.setSerializedContent(labelReference); + editor.tiptapEditor.commands.setContent( + '<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>', + ); expect(editor.getSerializedContent()).toBe(labelReference); }); diff --git a/spec/frontend/content_editor/services/markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js index bea43a0effc..5458a42532f 100644 --- a/spec/frontend/content_editor/services/markdown_deserializer_spec.js +++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js @@ -1,8 +1,8 @@ -import createMarkdownDeserializer from '~/content_editor/services/markdown_deserializer'; +import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer'; import Bold from '~/content_editor/extensions/bold'; import { createTestEditor, createDocBuilder } from '../test_utils'; -describe('content_editor/services/markdown_deserializer', () => { +describe('content_editor/services/gl_api_markdown_deserializer', () => { let renderMarkdown; let doc; let p; @@ -32,7 +32,9 @@ describe('content_editor/services/markdown_deserializer', () => { beforeEach(async () => { const deserializer = createMarkdownDeserializer({ render: renderMarkdown }); - renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`); + renderMarkdown.mockResolvedValueOnce( + `<p><strong>${text}</strong></p><pre lang="javascript"></pre>`, + ); result = await deserializer.deserialize({ content: 'content', @@ -40,13 +42,9 @@ describe('content_editor/services/markdown_deserializer', () => { }); }); it('transforms HTML returned by render function to a ProseMirror document', async () => { - const expectedDoc = doc(p(bold(text))); + const document = doc(p(bold(text))); - expect(result.document.toJSON()).toEqual(expectedDoc.toJSON()); - }); - - it('returns parsed HTML as a DOM object', () => { - expect(result.dom.innerHTML).toEqual(`<p><strong>${text}</strong></p><!--content-->`); + expect(result.document.toJSON()).toEqual(document.toJSON()); }); }); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 2b76dc6c984..25b7483f234 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -24,6 +24,7 @@ import Link from '~/content_editor/extensions/link'; import ListItem from '~/content_editor/extensions/list_item'; import OrderedList from '~/content_editor/extensions/ordered_list'; import Paragraph from '~/content_editor/extensions/paragraph'; +import Sourcemap from '~/content_editor/extensions/sourcemap'; import Strike from '~/content_editor/extensions/strike'; import Table from '~/content_editor/extensions/table'; import TableCell from '~/content_editor/extensions/table_cell'; @@ -32,6 +33,7 @@ import TableRow from '~/content_editor/extensions/table_row'; import TaskItem from '~/content_editor/extensions/task_item'; import TaskList from '~/content_editor/extensions/task_list'; import markdownSerializer from '~/content_editor/services/markdown_serializer'; +import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTestEditor, createDocBuilder } from '../test_utils'; jest.mock('~/emoji'); @@ -63,6 +65,7 @@ const tiptapEditor = createTestEditor({ Link, ListItem, OrderedList, + Sourcemap, Strike, Table, TableCell, @@ -151,8 +154,7 @@ const { const serialize = (...content) => markdownSerializer({}).serialize({ - schema: tiptapEditor.schema, - content: doc(...content).toJSON(), + doc: doc(...content), }); describe('markdownSerializer', () => { @@ -1159,4 +1161,42 @@ Oranges are orange [^1] `.trim(), ); }); + + it.each` + mark | content | modifiedContent + ${'bold'} | ${'**bold**'} | ${'**bold modified**'} + ${'bold'} | ${'__bold__'} | ${'__bold modified__'} + ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} + ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} + ${'italic'} | ${'_italic_'} | ${'_italic modified_'} + ${'italic'} | ${'*italic*'} | ${'*italic modified*'} + ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} + ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} + ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} + ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} + ${'code'} | ${'`code`'} | ${'`code modified`'} + ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} + `( + 'preserves original $mark syntax when sourceMarkdown is available', + async ({ content, modifiedContent }) => { + const { document } = await remarkMarkdownDeserializer().deserialize({ + schema: tiptapEditor.schema, + content, + }); + + tiptapEditor + .chain() + .setContent(document.toJSON()) + // changing the document ensures that block preservation doesn’t yield false positives + .insertContent(' modified') + .run(); + + const serialized = markdownSerializer({}).serialize({ + pristineDoc: document, + doc: tiptapEditor.state.doc, + }); + + expect(serialized).toEqual(modifiedContent); + }, + ); }); diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js index abd9588daff..8a304c73163 100644 --- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js +++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js @@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core'; import BulletList from '~/content_editor/extensions/bullet_list'; import ListItem from '~/content_editor/extensions/list_item'; import Paragraph from '~/content_editor/extensions/paragraph'; -import markdownDeserializer from '~/content_editor/services/markdown_deserializer'; +import markdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer'; import { getMarkdownSource, getFullSource } from '~/content_editor/services/markdown_sourcemap'; import { createTestEditor, createDocBuilder } from '../test_utils'; |