Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/content_editor')
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js17
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js77
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js63
-rw-r--r--spec/frontend/content_editor/components/loading_indicator_spec.js71
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js2
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js2
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js17
-rw-r--r--spec/frontend/content_editor/extensions/paste_markdown_spec.js127
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec_helper.js2
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js39
-rw-r--r--spec/frontend/content_editor/services/markdown_deserializer_spec.js62
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js13
-rw-r--r--spec/frontend/content_editor/services/markdown_sourcemap_spec.js29
-rw-r--r--spec/frontend/content_editor/test_utils.js20
15 files changed, 445 insertions, 98 deletions
diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index 2ddcd8f024e..12484cb13c6 100644
--- a/spec/frontend/content_editor/components/content_editor_alert_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -3,20 +3,25 @@ import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import { createTestEditor, emitEditorEvent } from '../test_utils';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import { ALERT_EVENT } from '~/content_editor/constants';
+import { createTestEditor } from '../test_utils';
describe('content_editor/components/content_editor_alert', () => {
let wrapper;
let tiptapEditor;
+ let eventHub;
const findErrorAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = async () => {
tiptapEditor = createTestEditor();
+ eventHub = eventHubFactory();
wrapper = shallowMountExtended(ContentEditorAlert, {
provide: {
tiptapEditor,
+ eventHub,
},
stubs: {
EditorStateObserver,
@@ -37,7 +42,9 @@ describe('content_editor/components/content_editor_alert', () => {
async ({ message, variant }) => {
createWrapper();
- await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } });
+ eventHub.$emit(ALERT_EVENT, { message, variant });
+
+ await nextTick();
expect(findErrorAlert().text()).toBe(message);
expect(findErrorAlert().attributes().variant).toBe(variant);
@@ -48,11 +55,9 @@ describe('content_editor/components/content_editor_alert', () => {
const message = 'error message';
createWrapper();
-
- await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } });
-
+ eventHub.$emit(ALERT_EVENT, { message });
+ await nextTick();
findErrorAlert().vm.$emit('dismiss');
-
await nextTick();
expect(findErrorAlert().exists()).toBe(false);
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 9a772c41e52..73fcfeab8bc 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,6 +1,4 @@
-import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent } from '@tiptap/vue-2';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
@@ -8,11 +6,7 @@ import ContentEditorProvider from '~/content_editor/components/content_editor_pr
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
-} from '~/content_editor/constants';
+import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import { emitEditorEvent } from '../test_utils';
jest.mock('~/emoji');
@@ -25,9 +19,6 @@ describe('ContentEditor', () => {
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findBubbleMenu = () => wrapper.findComponent(FormattingBubbleMenu);
-
const createWrapper = (propsData = {}) => {
renderMarkdown = jest.fn();
@@ -117,69 +108,15 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true);
});
- describe('when loading content', () => {
- beforeEach(async () => {
- createWrapper();
-
- contentEditor.emit(LOADING_CONTENT_EVENT);
-
- await nextTick();
- });
-
- it('displays loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('hides EditorContent component', () => {
- expect(findEditorContent().exists()).toBe(false);
- });
-
- it('hides formatting bubble menu', () => {
- expect(findBubbleMenu().exists()).toBe(false);
- });
- });
-
- describe('when loading content succeeds', () => {
- beforeEach(async () => {
- createWrapper();
-
- contentEditor.emit(LOADING_CONTENT_EVENT);
- await nextTick();
- contentEditor.emit(LOADING_SUCCESS_EVENT);
- await nextTick();
- });
-
- it('hides loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
+ it('renders loading indicator component', () => {
+ createWrapper();
- it('displays EditorContent component', () => {
- expect(findEditorContent().exists()).toBe(true);
- });
+ expect(wrapper.findComponent(LoadingIndicator).exists()).toBe(true);
});
- describe('when loading content fails', () => {
- const error = 'error';
-
- beforeEach(async () => {
- createWrapper();
-
- contentEditor.emit(LOADING_CONTENT_EVENT);
- await nextTick();
- contentEditor.emit(LOADING_ERROR_EVENT, error);
- await nextTick();
- });
-
- it('hides loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('displays EditorContent component', () => {
- expect(findEditorContent().exists()).toBe(true);
- });
+ it('renders formatting bubble menu', () => {
+ createWrapper();
- it('displays formatting bubble menu', () => {
- expect(findBubbleMenu().exists()).toBe(true);
- });
+ expect(wrapper.findComponent(FormattingBubbleMenu).exists()).toBe(true);
});
});
diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js
index 5e4bb348e1f..51a594a606b 100644
--- a/spec/frontend/content_editor/components/editor_state_observer_spec.js
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -3,6 +3,13 @@ import { each } from 'lodash';
import EditorStateObserver, {
tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import {
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+ ALERT_EVENT,
+} from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => {
@@ -11,19 +18,29 @@ describe('content_editor/components/editor_state_observer', () => {
let onDocUpdateListener;
let onSelectionUpdateListener;
let onTransactionListener;
+ let onLoadingContentListener;
+ let onLoadingSuccessListener;
+ let onLoadingErrorListener;
+ let onAlertListener;
+ let eventHub;
const buildEditor = () => {
tiptapEditor = createTestEditor();
+ eventHub = eventHubFactory();
jest.spyOn(tiptapEditor, 'on');
};
const buildWrapper = () => {
wrapper = shallowMount(EditorStateObserver, {
- provide: { tiptapEditor },
+ provide: { tiptapEditor, eventHub },
listeners: {
docUpdate: onDocUpdateListener,
selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener,
+ [ALERT_EVENT]: onAlertListener,
+ [LOADING_CONTENT_EVENT]: onLoadingContentListener,
+ [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener,
+ [LOADING_ERROR_EVENT]: onLoadingErrorListener,
},
});
};
@@ -32,8 +49,11 @@ describe('content_editor/components/editor_state_observer', () => {
onDocUpdateListener = jest.fn();
onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn();
+ onAlertListener = jest.fn();
+ onLoadingSuccessListener = jest.fn();
+ onLoadingContentListener = jest.fn();
+ onLoadingErrorListener = jest.fn();
buildEditor();
- buildWrapper();
});
afterEach(() => {
@@ -44,6 +64,8 @@ describe('content_editor/components/editor_state_observer', () => {
it('emits update, selectionUpdate, and transaction events', () => {
const content = '<p>My paragraph</p>';
+ buildWrapper();
+
tiptapEditor.commands.insertContent(content);
expect(onDocUpdateListener).toHaveBeenCalledWith(
@@ -58,10 +80,27 @@ describe('content_editor/components/editor_state_observer', () => {
});
});
+ it.each`
+ event | listener
+ ${ALERT_EVENT} | ${() => onAlertListener}
+ ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener}
+ ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener}
+ ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener}
+ `('listens to $event event in the eventBus object', ({ event, listener }) => {
+ const args = {};
+
+ buildWrapper();
+
+ eventHub.$emit(event, args);
+ expect(listener()).toHaveBeenCalledWith(args);
+ });
+
describe('when component is destroyed', () => {
it('removes onTiptapDocUpdate and onTiptapSelectionUpdate hooks', () => {
jest.spyOn(tiptapEditor, 'off');
+ buildWrapper();
+
wrapper.destroy();
each(tiptapToComponentMap, (_, tiptapEvent) => {
@@ -71,5 +110,25 @@ describe('content_editor/components/editor_state_observer', () => {
);
});
});
+
+ it.each`
+ event
+ ${ALERT_EVENT}
+ ${LOADING_CONTENT_EVENT}
+ ${LOADING_SUCCESS_EVENT}
+ ${LOADING_ERROR_EVENT}
+ `('removes $event event hook from eventHub', ({ event }) => {
+ jest.spyOn(eventHub, '$off');
+ jest.spyOn(eventHub, '$on');
+
+ buildWrapper();
+
+ wrapper.destroy();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ event,
+ eventHub.$on.mock.calls.find(([eventName]) => eventName === event)[1],
+ );
+ });
});
});
diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js
new file mode 100644
index 00000000000..e4fb09b70a4
--- /dev/null
+++ b/spec/frontend/content_editor/components/loading_indicator_spec.js
@@ -0,0 +1,71 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
+import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
+import {
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+} from '~/content_editor/constants';
+
+describe('content_editor/components/loading_indicator', () => {
+ let wrapper;
+
+ const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(LoadingIndicator);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when loading content', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
+
+ await nextTick();
+ });
+
+ it('displays loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when loading content succeeds', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
+ await nextTick();
+ findEditorStateObserver().vm.$emit(LOADING_SUCCESS_EVENT);
+ await nextTick();
+ });
+
+ it('hides loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when loading content fails', () => {
+ const error = 'error';
+
+ beforeEach(async () => {
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
+ await nextTick();
+ findEditorStateObserver().vm.$emit(LOADING_ERROR_EVENT, error);
+ await nextTick();
+ });
+
+ it('hides loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index 60263c46bdd..ce50482302d 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_button', () => {
@@ -25,6 +26,7 @@ describe('content_editor/components/toolbar_button', () => {
},
provide: {
tiptapEditor,
+ eventHub: eventHubFactory(),
},
propsData: {
contentType: CONTENT_TYPE,
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
index 0cf488260bd..fc26a9da471 100644
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
import Link from '~/content_editor/extensions/link';
import { hasSelection } from '~/content_editor/services/utils';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
@@ -15,6 +16,7 @@ describe('content_editor/components/toolbar_link_button', () => {
wrapper = mountExtended(ToolbarLinkButton, {
provide: {
tiptapEditor: editor,
+ eventHub: eventHubFactory(),
},
});
};
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
index 65c1c8c8310..608be1bd693 100644
--- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -4,6 +4,7 @@ import EditorStateObserver from '~/content_editor/components/editor_state_observ
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
import Heading from '~/content_editor/extensions/heading';
+import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_text_style_dropdown', () => {
@@ -27,6 +28,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
},
provide: {
tiptapEditor,
+ eventHub: eventHubFactory(),
},
propsData: {
...propsData,
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d2d2cd98a78..ec67545cf17 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -4,7 +4,9 @@ import Attachment from '~/content_editor/extensions/attachment';
import Image from '~/content_editor/extensions/image';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
+import { VARIANT_DANGER } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
+import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
@@ -25,6 +27,7 @@ describe('content_editor/extensions/attachment', () => {
let link;
let renderMarkdown;
let mock;
+ let eventHub;
const uploadsPath = '/uploads/';
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
@@ -50,9 +53,15 @@ describe('content_editor/extensions/attachment', () => {
beforeEach(() => {
renderMarkdown = jest.fn();
+ eventHub = eventHubFactory();
tiptapEditor = createTestEditor({
- extensions: [Loading, Link, Image, Attachment.configure({ renderMarkdown, uploadsPath })],
+ extensions: [
+ Loading,
+ Link,
+ Image,
+ Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
+ ],
});
({
@@ -160,7 +169,8 @@ describe('content_editor/extensions/attachment', () => {
it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile });
- tiptapEditor.on('alert', ({ message }) => {
+ eventHub.$on('alert', ({ message, variant }) => {
+ expect(variant).toBe(VARIANT_DANGER);
expect(message).toBe('An error occurred while uploading the image. Please try again.');
done();
});
@@ -236,7 +246,8 @@ describe('content_editor/extensions/attachment', () => {
it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
- tiptapEditor.on('alert', ({ message }) => {
+ eventHub.$on('alert', ({ message, variant }) => {
+ expect(variant).toBe(VARIANT_DANGER);
expect(message).toBe('An error occurred while uploading the file. Please try again.');
done();
});
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
new file mode 100644
index 00000000000..8f734c7dabc
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
@@ -0,0 +1,127 @@
+import PasteMarkdown from '~/content_editor/extensions/paste_markdown';
+import Bold from '~/content_editor/extensions/bold';
+import { VARIANT_DANGER } from '~/flash';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import {
+ ALERT_EVENT,
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+} from '~/content_editor/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
+
+describe('content_editor/extensions/paste_markdown', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let bold;
+ let renderMarkdown;
+ let eventHub;
+ const defaultData = { 'text/plain': '**bold text**' };
+
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ eventHub = eventHubFactory();
+
+ jest.spyOn(eventHub, '$emit');
+
+ tiptapEditor = createTestEditor({
+ extensions: [PasteMarkdown.configure({ renderMarkdown, eventHub }), Bold],
+ });
+
+ ({
+ builders: { doc, p, bold },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ Bold: { markType: Bold.name },
+ },
+ }));
+ });
+
+ const buildClipboardEvent = ({ data = {}, types = ['text/plain'] } = {}) => {
+ return Object.assign(new Event('paste'), {
+ clipboardData: { types, getData: jest.fn((type) => data[type] || defaultData[type]) },
+ });
+ };
+
+ const triggerPasteEventHandler = (event) => {
+ let handled = false;
+
+ tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
+ handled = eventHandler(tiptapEditor.view, event);
+ });
+
+ return handled;
+ };
+
+ const triggerPasteEventHandlerAndWaitForTransaction = (event) => {
+ return waitUntilNextDocTransaction({
+ tiptapEditor,
+ action: () => {
+ tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
+ return eventHandler(tiptapEditor.view, event);
+ });
+ },
+ });
+ };
+
+ it.each`
+ types | data | handled | desc
+ ${['text/plain']} | ${{}} | ${true} | ${'handles plain text'}
+ ${['text/plain', 'text/html']} | ${{}} | ${false} | ${'doesn’t handle html format'}
+ ${['text/plain', 'text/html', 'vscode-editor-data']} | ${{ 'vscode-editor-data': '{ "mode": "markdown" }' }} | ${true} | ${'handles vscode markdown'}
+ ${['text/plain', 'text/html', 'vscode-editor-data']} | ${{ 'vscode-editor-data': '{ "mode": "ruby" }' }} | ${false} | ${'doesn’t vscode code snippet'}
+ `('$desc', ({ types, handled, data }) => {
+ expect(triggerPasteEventHandler(buildClipboardEvent({ types, data }))).toBe(handled);
+ });
+
+ describe('when pasting raw markdown source', () => {
+ describe('when rendering markdown succeeds', () => {
+ beforeEach(() => {
+ renderMarkdown.mockResolvedValueOnce('<strong>bold text</strong>');
+ });
+
+ it('transforms pasted text into a prosemirror node', async () => {
+ const expectedDoc = doc(p(bold('bold text')));
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it(`triggers ${LOADING_SUCCESS_EVENT}`, async () => {
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_CONTENT_EVENT);
+ expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_SUCCESS_EVENT);
+ });
+ });
+
+ describe('when rendering markdown fails', () => {
+ beforeEach(() => {
+ renderMarkdown.mockRejectedValueOnce();
+ });
+
+ it(`triggers ${LOADING_ERROR_EVENT} event`, async () => {
+ triggerPasteEventHandler(buildClipboardEvent());
+
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_ERROR_EVENT);
+ });
+
+ it(`triggers ${ALERT_EVENT} event`, async () => {
+ triggerPasteEventHandler(buildClipboardEvent());
+
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(ALERT_EVENT, {
+ message: expect.any(String),
+ variant: VARIANT_DANGER,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/markdown_processing_spec_helper.js b/spec/frontend/content_editor/markdown_processing_spec_helper.js
index bb7ec0030a2..41442dd8388 100644
--- a/spec/frontend/content_editor/markdown_processing_spec_helper.js
+++ b/spec/frontend/content_editor/markdown_processing_spec_helper.js
@@ -55,7 +55,7 @@ const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
// Assert that the markdown we ended up with after sending it through all the ContentEditor
// plumbing matches the original markdown from the YAML.
- expect(serializedContent).toBe(markdown);
+ expect(serializedContent.trim()).toBe(markdown.trim());
};
// describeMarkdownProcesssing
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index e48687f1548..3bc72b13302 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -4,19 +4,31 @@ import {
LOADING_ERROR_EVENT,
} from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor';
-
-import { createTestEditor } from '../test_utils';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
+ let deserializer;
+ let eventHub;
+ let doc;
+ let p;
beforeEach(() => {
const tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'destroy');
+ ({
+ builders: { doc, p },
+ } = createDocBuilder({
+ tiptapEditor,
+ }));
+
serializer = { deserialize: jest.fn() };
- contentEditor = new ContentEditor({ tiptapEditor, serializer });
+ deserializer = { deserialize: jest.fn() };
+ eventHub = eventHubFactory();
+ contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub });
});
describe('.dispose', () => {
@@ -30,33 +42,42 @@ describe('content_editor/services/content_editor', () => {
});
describe('when setSerializedContent succeeds', () => {
+ let document;
+
beforeEach(() => {
- serializer.deserialize.mockResolvedValueOnce('');
+ document = doc(p('document'));
+ deserializer.deserialize.mockResolvedValueOnce({ document });
});
- it('emits loadingContent and loadingSuccess event', () => {
+ it('emits loadingContent and loadingSuccess event in the eventHub', () => {
let loadingContentEmitted = false;
- contentEditor.on(LOADING_CONTENT_EVENT, () => {
+ eventHub.$on(LOADING_CONTENT_EVENT, () => {
loadingContentEmitted = true;
});
- contentEditor.on(LOADING_SUCCESS_EVENT, () => {
+ eventHub.$on(LOADING_SUCCESS_EVENT, () => {
expect(loadingContentEmitted).toBe(true);
});
contentEditor.setSerializedContent('**bold text**');
});
+
+ it('sets the deserialized document in the tiptap editor object', async () => {
+ await contentEditor.setSerializedContent('**bold text**');
+
+ expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
+ });
});
describe('when setSerializedContent fails', () => {
const error = 'error';
beforeEach(() => {
- serializer.deserialize.mockRejectedValueOnce(error);
+ deserializer.deserialize.mockRejectedValueOnce(error);
});
it('emits loadingError event', async () => {
- contentEditor.on(LOADING_ERROR_EVENT, (e) => {
+ eventHub.$on(LOADING_ERROR_EVENT, (e) => {
expect(e).toBe('error');
});
diff --git a/spec/frontend/content_editor/services/markdown_deserializer_spec.js b/spec/frontend/content_editor/services/markdown_deserializer_spec.js
new file mode 100644
index 00000000000..bea43a0effc
--- /dev/null
+++ b/spec/frontend/content_editor/services/markdown_deserializer_spec.js
@@ -0,0 +1,62 @@
+import createMarkdownDeserializer from '~/content_editor/services/markdown_deserializer';
+import Bold from '~/content_editor/extensions/bold';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/services/markdown_deserializer', () => {
+ let renderMarkdown;
+ let doc;
+ let p;
+ let bold;
+ let tiptapEditor;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({
+ extensions: [Bold],
+ });
+
+ ({
+ builders: { doc, p, bold },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ bold: { markType: Bold.name },
+ },
+ }));
+ renderMarkdown = jest.fn();
+ });
+
+ describe('when deserializing', () => {
+ let result;
+ const text = 'Bold text';
+
+ beforeEach(async () => {
+ const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
+
+ renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`);
+
+ result = await deserializer.deserialize({
+ content: 'content',
+ schema: tiptapEditor.schema,
+ });
+ });
+ it('transforms HTML returned by render function to a ProseMirror document', async () => {
+ const expectedDoc = 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-->`);
+ });
+ });
+
+ describe('when the render function returns an empty value', () => {
+ it('returns an empty object', async () => {
+ const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
+
+ renderMarkdown.mockResolvedValueOnce(null);
+
+ expect(await deserializer.deserialize({ content: 'content' })).toEqual({});
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 01d4c994e88..2b76dc6c984 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -597,6 +597,7 @@ this is not really json but just trying out whether this case works or not
paragraph('A giant ', italic('owl-like'), ' creature.'),
),
),
+ heading('this is a heading'),
),
).toBe(
`
@@ -612,6 +613,8 @@ A giant _owl-like_ creature.
</dd>
</dl>
+
+# this is a heading
`.trim(),
);
});
@@ -623,6 +626,7 @@ A giant _owl-like_ creature.
detailsContent(paragraph('this is the summary')),
detailsContent(paragraph('this content will be hidden')),
),
+ heading('this is a heading'),
),
).toBe(
`
@@ -630,6 +634,8 @@ A giant _owl-like_ creature.
<summary>this is the summary</summary>
this content will be hidden
</details>
+
+# this is a heading
`.trim(),
);
});
@@ -648,7 +654,7 @@ this content will be hidden
detailsContent(paragraph('this content will be ', italic('hidden'))),
),
details(detailsContent(paragraph('summary 2')), detailsContent(paragraph('content 2'))),
- ),
+ ).trim(),
).toBe(
`
<details>
@@ -669,6 +675,7 @@ console.log(c);
this content will be _hidden_
</details>
+
<details>
<summary>summary 2</summary>
content 2
@@ -694,7 +701,7 @@ content 2
),
),
),
- ),
+ ).trim(),
).toBe(
`
<details>
@@ -709,7 +716,9 @@ content 2
_inception_
</details>
+
</details>
+
</details>
`.trim(),
);
diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
index 6f908f468f6..abd9588daff 100644
--- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
+++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
@@ -2,8 +2,8 @@ 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 markdownSerializer from '~/content_editor/services/markdown_serializer';
-import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
+import markdownDeserializer from '~/content_editor/services/markdown_deserializer';
+import { getMarkdownSource, getFullSource } from '~/content_editor/services/markdown_sourcemap';
import { createTestEditor, createDocBuilder } from '../test_utils';
const BULLET_LIST_MARKDOWN = `+ list item 1
@@ -52,10 +52,29 @@ const {
});
describe('content_editor/services/markdown_sourcemap', () => {
+ describe('getFullSource', () => {
+ it.each`
+ lastChild | expected
+ ${null} | ${[]}
+ ${{ nodeName: 'paragraph' }} | ${[]}
+ ${{ nodeName: '#comment', textContent: null }} | ${[]}
+ ${{ nodeName: '#comment', textContent: '+ list item 1\n+ list item 2' }} | ${['+ list item 1', '+ list item 2']}
+ `('with lastChild=$lastChild, returns $expected', ({ lastChild, expected }) => {
+ const element = {
+ ownerDocument: {
+ body: {
+ lastChild,
+ },
+ },
+ };
+
+ expect(getFullSource(element)).toEqual(expected);
+ });
+ });
+
it('gets markdown source for a rendered HTML element', async () => {
- const deserialized = await markdownSerializer({
+ const { document } = await markdownDeserializer({
render: () => BULLET_LIST_HTML,
- serializerConfig: {},
}).deserialize({
schema: tiptapEditor.schema,
content: BULLET_LIST_MARKDOWN,
@@ -76,6 +95,6 @@ describe('content_editor/services/markdown_sourcemap', () => {
),
);
- expect(deserialized).toEqual(expected.toJSON());
+ expect(document.toJSON()).toEqual(expected.toJSON());
});
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 84eaa3c5f44..dde9d738235 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -142,3 +142,23 @@ export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
f(view, selection.from, inputRuleText.length + 1, inputRuleText),
);
};
+
+/**
+ * Executes an action that triggers a transaction in the
+ * tiptap Editor. Returns a promise that resolves
+ * after the transaction completes
+ * @param {*} params.tiptapEditor Tiptap editor
+ * @param {*} params.action A function that triggers a transaction in the tiptap Editor
+ * @returns A promise that resolves when the transaction completes
+ */
+export const waitUntilNextDocTransaction = ({ tiptapEditor, action }) => {
+ return new Promise((resolve) => {
+ const handleTransaction = () => {
+ tiptapEditor.off('update', handleTransaction);
+ resolve();
+ };
+
+ tiptapEditor.on('update', handleTransaction);
+ action();
+ });
+};