Commit 31977e5d authored by Enrique Alcantara's avatar Enrique Alcantara

Refactor eventHub in the Content Editor

Allow to share the eventHub object between
the Content Editor facade object and the
Content Editor extensions

The purpose of this refactoring is allowing
to trigger the content loading events from
the paste markdown extension
parent 188532e3
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2'; import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
import { createContentEditor } from '../services/create_content_editor'; import { createContentEditor } from '../services/create_content_editor';
import ContentEditorAlert from './content_editor_alert.vue'; import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue'; import ContentEditorProvider from './content_editor_provider.vue';
...@@ -55,17 +54,12 @@ export default { ...@@ -55,17 +54,12 @@ export default {
extensions, extensions,
serializerConfig, serializerConfig,
}); });
},
this.contentEditor.on(LOADING_CONTENT_EVENT, this.displayLoadingIndicator); mounted() {
this.contentEditor.on(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
this.contentEditor.on(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
this.$emit('initialized', this.contentEditor); this.$emit('initialized', this.contentEditor);
}, },
beforeDestroy() { beforeDestroy() {
this.contentEditor.dispose(); this.contentEditor.dispose();
this.contentEditor.off(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
this.contentEditor.off(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
this.contentEditor.off(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
}, },
methods: { methods: {
displayLoadingIndicator() { displayLoadingIndicator() {
...@@ -91,7 +85,14 @@ export default { ...@@ -91,7 +85,14 @@ export default {
<template> <template>
<content-editor-provider :content-editor="contentEditor"> <content-editor-provider :content-editor="contentEditor">
<div> <div>
<editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" /> <editor-state-observer
@loading="displayLoadingIndicator"
@loadingSuccess="hideLoadingIndicator"
@loadingError="hideLoadingIndicator"
@docUpdate="notifyChange"
@focus="focus"
@blur="blur"
/>
<content-editor-alert /> <content-editor-alert />
<div <div
data-testid="content-editor" data-testid="content-editor"
......
...@@ -8,6 +8,7 @@ export default { ...@@ -8,6 +8,7 @@ export default {
return { return {
contentEditor, contentEditor,
eventHub: contentEditor.eventHub,
tiptapEditor: contentEditor.tiptapEditor, tiptapEditor: contentEditor.tiptapEditor,
}; };
}, },
......
<script> <script>
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import {
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
ALERT_EVENT,
} from '../constants';
export const tiptapToComponentMap = { export const tiptapToComponentMap = {
update: 'docUpdate', update: 'docUpdate',
...@@ -7,30 +13,48 @@ export const tiptapToComponentMap = { ...@@ -7,30 +13,48 @@ export const tiptapToComponentMap = {
transaction: 'transaction', transaction: 'transaction',
focus: 'focus', focus: 'focus',
blur: 'blur', blur: 'blur',
alert: 'alert',
}; };
export const eventHubEvents = [
ALERT_EVENT,
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
];
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName]; const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
export default { export default {
inject: ['tiptapEditor'], inject: ['tiptapEditor', 'eventHub'],
created() { created() {
this.disposables = []; this.disposables = [];
Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => { Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
const eventHandler = debounce((params) => this.handleTipTapEvent(tiptapEvent, params), 100); const eventHandler = debounce(
(params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params),
100,
);
this.tiptapEditor?.on(tiptapEvent, eventHandler); this.tiptapEditor?.on(tiptapEvent, eventHandler);
this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler)); this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler));
}); });
eventHubEvents.forEach((event) => {
const handler = (...params) => {
this.bubbleEvent(event, ...params);
};
this.eventHub.$on(event, handler);
this.disposables.push(() => this.eventHub?.$off(event, handler));
});
}, },
beforeDestroy() { beforeDestroy() {
this.disposables.forEach((dispose) => dispose()); this.disposables.forEach((dispose) => dispose());
}, },
methods: { methods: {
handleTipTapEvent(tiptapEvent, params) { bubbleEvent(eventHubEvent, params) {
this.$emit(getComponentEventName(tiptapEvent), params); this.$emit(eventHubEvent, params);
}, },
}, },
render() { render() {
......
...@@ -42,9 +42,10 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [ ...@@ -42,9 +42,10 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
}, },
]; ];
export const LOADING_CONTENT_EVENT = 'loadingContent'; export const LOADING_CONTENT_EVENT = 'loading';
export const LOADING_SUCCESS_EVENT = 'loadingSuccess'; export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
export const LOADING_ERROR_EVENT = 'loadingError'; export const LOADING_ERROR_EVENT = 'loadingError';
export const ALERT_EVENT = 'alert';
export const PARSE_HTML_PRIORITY_LOWEST = 1; export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50; export const PARSE_HTML_PRIORITY_DEFAULT = 50;
...@@ -56,3 +57,4 @@ export const EXTENSION_PRIORITY_LOWER = 75; ...@@ -56,3 +57,4 @@ export const EXTENSION_PRIORITY_LOWER = 75;
* https://tiptap.dev/guide/custom-extensions/#priority * https://tiptap.dev/guide/custom-extensions/#priority
*/ */
export const EXTENSION_PRIORITY_DEFAULT = 100; export const EXTENSION_PRIORITY_DEFAULT = 100;
export const EXTENSION_PRIORITY_HIGHEST = 200;
...@@ -9,15 +9,22 @@ export default Extension.create({ ...@@ -9,15 +9,22 @@ export default Extension.create({
return { return {
uploadsPath: null, uploadsPath: null,
renderMarkdown: null, renderMarkdown: null,
eventHub: null,
}; };
}, },
addCommands() { addCommands() {
return { return {
uploadAttachment: ({ file }) => () => { uploadAttachment: ({ file }) => () => {
const { uploadsPath, renderMarkdown } = this.options; const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor }); return handleFileEvent({
file,
uploadsPath,
renderMarkdown,
editor: this.editor,
eventHub,
});
}, },
}; };
}, },
...@@ -29,23 +36,25 @@ export default Extension.create({ ...@@ -29,23 +36,25 @@ export default Extension.create({
key: new PluginKey('attachment'), key: new PluginKey('attachment'),
props: { props: {
handlePaste: (_, event) => { handlePaste: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options; const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({ return handleFileEvent({
editor, editor,
file: event.clipboardData.files[0], file: event.clipboardData.files[0],
uploadsPath, uploadsPath,
renderMarkdown, renderMarkdown,
eventHub,
}); });
}, },
handleDrop: (_, event) => { handleDrop: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options; const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({ return handleFileEvent({
editor, editor,
file: event.dataTransfer.files[0], file: event.dataTransfer.files[0],
uploadsPath, uploadsPath,
renderMarkdown, renderMarkdown,
eventHub,
}); });
}, },
}, },
......
import eventHubFactory from '~/helpers/event_hub_factory';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants'; import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
export class ContentEditor { export class ContentEditor {
constructor({ tiptapEditor, serializer }) { constructor({ tiptapEditor, serializer, eventHub }) {
this._tiptapEditor = tiptapEditor; this._tiptapEditor = tiptapEditor;
this._serializer = serializer; this._serializer = serializer;
this._eventHub = eventHubFactory(); this._eventHub = eventHub;
} }
get tiptapEditor() { get tiptapEditor() {
return this._tiptapEditor; return this._tiptapEditor;
} }
get eventHub() {
return this._eventHub;
}
get empty() { get empty() {
const doc = this.tiptapEditor?.state.doc; const doc = this.tiptapEditor?.state.doc;
...@@ -23,39 +26,23 @@ export class ContentEditor { ...@@ -23,39 +26,23 @@ export class ContentEditor {
this.tiptapEditor.destroy(); this.tiptapEditor.destroy();
} }
once(type, handler) {
this._eventHub.$once(type, handler);
}
on(type, handler) {
this._eventHub.$on(type, handler);
}
emit(type, params = {}) {
this._eventHub.$emit(type, params);
}
off(type, handler) {
this._eventHub.$off(type, handler);
}
disposeAllEvents() { disposeAllEvents() {
this._eventHub.dispose(); this._eventHub.dispose();
} }
async setSerializedContent(serializedContent) { async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _serializer: serializer } = this; const { _tiptapEditor: editor, _serializer: serializer, _eventHub: eventHub } = this;
try { try {
this._eventHub.$emit(LOADING_CONTENT_EVENT); eventHub.$emit(LOADING_CONTENT_EVENT);
const document = await serializer.deserialize({ const document = await serializer.deserialize({
schema: editor.schema, schema: editor.schema,
content: serializedContent, content: serializedContent,
}); });
editor.commands.setContent(document); editor.commands.setContent(document);
this._eventHub.$emit(LOADING_SUCCESS_EVENT); eventHub.$emit(LOADING_SUCCESS_EVENT);
} catch (e) { } catch (e) {
this._eventHub.$emit(LOADING_ERROR_EVENT, e); eventHub.$emit(LOADING_ERROR_EVENT, e);
throw e; throw e;
} }
} }
......
import { Editor } from '@tiptap/vue-2'; import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash'; import { isFunction } from 'lodash';
import eventHubFactory from '~/helpers/event_hub_factory';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants'; import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment'; import Attachment from '../extensions/attachment';
import Audio from '../extensions/audio'; import Audio from '../extensions/audio';
...@@ -78,8 +79,10 @@ export const createContentEditor = ({ ...@@ -78,8 +79,10 @@ export const createContentEditor = ({
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR); throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
} }
const eventHub = eventHubFactory();
const builtInContentEditorExtensions = [ const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown }), Attachment.configure({ uploadsPath, renderMarkdown, eventHub }),
Audio, Audio,
Blockquote, Blockquote,
Bold, Bold,
...@@ -137,5 +140,5 @@ export const createContentEditor = ({ ...@@ -137,5 +140,5 @@ export const createContentEditor = ({
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions }); const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig }); const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
return new ContentEditor({ tiptapEditor, serializer }); return new ContentEditor({ tiptapEditor, serializer, eventHub });
}; };
...@@ -49,7 +49,7 @@ export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => { ...@@ -49,7 +49,7 @@ export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
return extractAttachmentLinkUrl(rendered); return extractAttachmentLinkUrl(rendered);
}; };
const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => { const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
const encodedSrc = await readFileAsDataURL(file); const encodedSrc = await readFileAsDataURL(file);
const { view } = editor; const { view } = editor;
...@@ -72,14 +72,14 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => { ...@@ -72,14 +72,14 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
); );
} catch (e) { } catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 }); editor.commands.deleteRange({ from: position, to: position + 1 });
editor.emit('alert', { eventHub.$emit('alert', {
message: __('An error occurred while uploading the image. Please try again.'), message: __('An error occurred while uploading the image. Please try again.'),
variant: 'danger', variant: 'danger',
}); });
} }
}; };
const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) => { const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
await Promise.resolve(); await Promise.resolve();
const { view } = editor; const { view } = editor;
...@@ -103,23 +103,23 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) = ...@@ -103,23 +103,23 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
); );
} catch (e) { } catch (e) {
editor.commands.deleteRange({ from, to: from + 1 }); editor.commands.deleteRange({ from, to: from + 1 });
editor.emit('alert', { eventHub.$emit('alert', {
message: __('An error occurred while uploading the file. Please try again.'), message: __('An error occurred while uploading the file. Please try again.'),
variant: 'danger', variant: 'danger',
}); });
} }
}; };
export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => { export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
if (!file) return false; if (!file) return false;
if (acceptedMimes.image.includes(file?.type)) { if (acceptedMimes.image.includes(file?.type)) {
uploadImage({ editor, file, uploadsPath, renderMarkdown }); uploadImage({ editor, file, uploadsPath, renderMarkdown, eventHub });
return true; return true;
} }
uploadAttachment({ editor, file, uploadsPath, renderMarkdown }); uploadAttachment({ editor, file, uploadsPath, renderMarkdown, eventHub });
return true; return true;
}; };
...@@ -3,20 +3,25 @@ import { nextTick } from 'vue'; ...@@ -3,20 +3,25 @@ import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.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', () => { describe('content_editor/components/content_editor_alert', () => {
let wrapper; let wrapper;
let tiptapEditor; let tiptapEditor;
let eventHub;
const findErrorAlert = () => wrapper.findComponent(GlAlert); const findErrorAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = async () => { const createWrapper = async () => {
tiptapEditor = createTestEditor(); tiptapEditor = createTestEditor();
eventHub = eventHubFactory();
wrapper = shallowMountExtended(ContentEditorAlert, { wrapper = shallowMountExtended(ContentEditorAlert, {
provide: { provide: {
tiptapEditor, tiptapEditor,
eventHub,
}, },
stubs: { stubs: {
EditorStateObserver, EditorStateObserver,
...@@ -37,7 +42,9 @@ describe('content_editor/components/content_editor_alert', () => { ...@@ -37,7 +42,9 @@ describe('content_editor/components/content_editor_alert', () => {
async ({ message, variant }) => { async ({ message, variant }) => {
createWrapper(); createWrapper();
await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } }); eventHub.$emit(ALERT_EVENT, { message, variant });
await nextTick();
expect(findErrorAlert().text()).toBe(message); expect(findErrorAlert().text()).toBe(message);
expect(findErrorAlert().attributes().variant).toBe(variant); expect(findErrorAlert().attributes().variant).toBe(variant);
...@@ -48,11 +55,9 @@ describe('content_editor/components/content_editor_alert', () => { ...@@ -48,11 +55,9 @@ describe('content_editor/components/content_editor_alert', () => {
const message = 'error message'; const message = 'error message';
createWrapper(); createWrapper();
eventHub.$emit(ALERT_EVENT, { message });
await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } }); await nextTick();
findErrorAlert().vm.$emit('dismiss'); findErrorAlert().vm.$emit('dismiss');
await nextTick(); await nextTick();
expect(findErrorAlert().exists()).toBe(false); expect(findErrorAlert().exists()).toBe(false);
......
...@@ -121,7 +121,7 @@ describe('ContentEditor', () => { ...@@ -121,7 +121,7 @@ describe('ContentEditor', () => {
beforeEach(async () => { beforeEach(async () => {
createWrapper(); createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT); contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT);
await nextTick(); await nextTick();
}); });
...@@ -143,9 +143,9 @@ describe('ContentEditor', () => { ...@@ -143,9 +143,9 @@ describe('ContentEditor', () => {
beforeEach(async () => { beforeEach(async () => {
createWrapper(); createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT); contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT);
await nextTick(); await nextTick();
contentEditor.emit(LOADING_SUCCESS_EVENT); contentEditor.eventHub.$emit(LOADING_SUCCESS_EVENT);
await nextTick(); await nextTick();
}); });
...@@ -164,9 +164,9 @@ describe('ContentEditor', () => { ...@@ -164,9 +164,9 @@ describe('ContentEditor', () => {
beforeEach(async () => { beforeEach(async () => {
createWrapper(); createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT); contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT);
await nextTick(); await nextTick();
contentEditor.emit(LOADING_ERROR_EVENT, error); contentEditor.eventHub.$emit(LOADING_ERROR_EVENT, error);
await nextTick(); await nextTick();
}); });
......
...@@ -3,6 +3,13 @@ import { each } from 'lodash'; ...@@ -3,6 +3,13 @@ import { each } from 'lodash';
import EditorStateObserver, { import EditorStateObserver, {
tiptapToComponentMap, tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue'; } 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'; import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => { describe('content_editor/components/editor_state_observer', () => {
...@@ -11,19 +18,29 @@ describe('content_editor/components/editor_state_observer', () => { ...@@ -11,19 +18,29 @@ describe('content_editor/components/editor_state_observer', () => {
let onDocUpdateListener; let onDocUpdateListener;
let onSelectionUpdateListener; let onSelectionUpdateListener;
let onTransactionListener; let onTransactionListener;
let onLoadingContentListener;
let onLoadingSuccessListener;
let onLoadingErrorListener;
let onAlertListener;
let eventHub;
const buildEditor = () => { const buildEditor = () => {
tiptapEditor = createTestEditor(); tiptapEditor = createTestEditor();
eventHub = eventHubFactory();
jest.spyOn(tiptapEditor, 'on'); jest.spyOn(tiptapEditor, 'on');
}; };
const buildWrapper = () => { const buildWrapper = () => {
wrapper = shallowMount(EditorStateObserver, { wrapper = shallowMount(EditorStateObserver, {
provide: { tiptapEditor }, provide: { tiptapEditor, eventHub },
listeners: { listeners: {
docUpdate: onDocUpdateListener, docUpdate: onDocUpdateListener,
selectionUpdate: onSelectionUpdateListener, selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener, 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', () => { ...@@ -32,8 +49,11 @@ describe('content_editor/components/editor_state_observer', () => {
onDocUpdateListener = jest.fn(); onDocUpdateListener = jest.fn();
onSelectionUpdateListener = jest.fn(); onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn(); onTransactionListener = jest.fn();
onAlertListener = jest.fn();
onLoadingSuccessListener = jest.fn();
onLoadingContentListener = jest.fn();
onLoadingErrorListener = jest.fn();
buildEditor(); buildEditor();
buildWrapper();
}); });
afterEach(() => { afterEach(() => {
...@@ -44,6 +64,8 @@ describe('content_editor/components/editor_state_observer', () => { ...@@ -44,6 +64,8 @@ describe('content_editor/components/editor_state_observer', () => {
it('emits update, selectionUpdate, and transaction events', () => { it('emits update, selectionUpdate, and transaction events', () => {
const content = '<p>My paragraph</p>'; const content = '<p>My paragraph</p>';
buildWrapper();
tiptapEditor.commands.insertContent(content); tiptapEditor.commands.insertContent(content);
expect(onDocUpdateListener).toHaveBeenCalledWith( expect(onDocUpdateListener).toHaveBeenCalledWith(
...@@ -58,10 +80,27 @@ describe('content_editor/components/editor_state_observer', () => { ...@@ -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', () => { describe('when component is destroyed', () => {
it('removes onTiptapDocUpdate and onTiptapSelectionUpdate hooks', () => { it('removes onTiptapDocUpdate and onTiptapSelectionUpdate hooks', () => {
jest.spyOn(tiptapEditor, 'off'); jest.spyOn(tiptapEditor, 'off');
buildWrapper();
wrapper.destroy(); wrapper.destroy();
each(tiptapToComponentMap, (_, tiptapEvent) => { each(tiptapToComponentMap, (_, tiptapEvent) => {
...@@ -71,5 +110,25 @@ describe('content_editor/components/editor_state_observer', () => { ...@@ -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],
);
});
}); });
}); });
...@@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui'; ...@@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue'; import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_button', () => { describe('content_editor/components/toolbar_button', () => {
...@@ -25,6 +26,7 @@ describe('content_editor/components/toolbar_button', () => { ...@@ -25,6 +26,7 @@ describe('content_editor/components/toolbar_button', () => {
}, },
provide: { provide: {
tiptapEditor, tiptapEditor,
eventHub: eventHubFactory(),
}, },
propsData: { propsData: {
contentType: CONTENT_TYPE, contentType: CONTENT_TYPE,
......
import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui'; import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue'; 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 Link from '~/content_editor/extensions/link';
import { hasSelection } from '~/content_editor/services/utils'; import { hasSelection } from '~/content_editor/services/utils';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
...@@ -15,6 +16,7 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -15,6 +16,7 @@ describe('content_editor/components/toolbar_link_button', () => {
wrapper = mountExtended(ToolbarLinkButton, { wrapper = mountExtended(ToolbarLinkButton, {
provide: { provide: {
tiptapEditor: editor, tiptapEditor: editor,
eventHub: eventHubFactory(),
}, },
}); });
}; };
......
...@@ -4,6 +4,7 @@ import EditorStateObserver from '~/content_editor/components/editor_state_observ ...@@ -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 ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants'; import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
import Heading from '~/content_editor/extensions/heading'; import Heading from '~/content_editor/extensions/heading';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_text_style_dropdown', () => { describe('content_editor/components/toolbar_text_style_dropdown', () => {
...@@ -27,6 +28,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => { ...@@ -27,6 +28,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
}, },
provide: { provide: {
tiptapEditor, tiptapEditor,
eventHub: eventHubFactory(),
}, },
propsData: { propsData: {
...propsData, ...propsData,
......
...@@ -5,6 +5,7 @@ import Image from '~/content_editor/extensions/image'; ...@@ -5,6 +5,7 @@ import Image from '~/content_editor/extensions/image';
import Link from '~/content_editor/extensions/link'; import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading'; import Loading from '~/content_editor/extensions/loading';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils'; import { createTestEditor, createDocBuilder } from '../test_utils';
const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto"> const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
...@@ -25,6 +26,7 @@ describe('content_editor/extensions/attachment', () => { ...@@ -25,6 +26,7 @@ describe('content_editor/extensions/attachment', () => {
let link; let link;
let renderMarkdown; let renderMarkdown;
let mock; let mock;
let eventHub;
const uploadsPath = '/uploads/'; const uploadsPath = '/uploads/';
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' }); const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
...@@ -50,9 +52,15 @@ describe('content_editor/extensions/attachment', () => { ...@@ -50,9 +52,15 @@ describe('content_editor/extensions/attachment', () => {
beforeEach(() => { beforeEach(() => {
renderMarkdown = jest.fn(); renderMarkdown = jest.fn();
eventHub = eventHubFactory();
tiptapEditor = createTestEditor({ tiptapEditor = createTestEditor({
extensions: [Loading, Link, Image, Attachment.configure({ renderMarkdown, uploadsPath })], extensions: [
Loading,
Link,
Image,
Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
],
}); });
({ ({
...@@ -160,7 +168,7 @@ describe('content_editor/extensions/attachment', () => { ...@@ -160,7 +168,7 @@ describe('content_editor/extensions/attachment', () => {
it('emits an alert event that includes an error message', (done) => { it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile }); tiptapEditor.commands.uploadAttachment({ file: imageFile });
tiptapEditor.on('alert', ({ message }) => { eventHub.$on('alert', ({ message }) => {
expect(message).toBe('An error occurred while uploading the image. Please try again.'); expect(message).toBe('An error occurred while uploading the image. Please try again.');
done(); done();
}); });
...@@ -236,7 +244,7 @@ describe('content_editor/extensions/attachment', () => { ...@@ -236,7 +244,7 @@ describe('content_editor/extensions/attachment', () => {
it('emits an alert event that includes an error message', (done) => { it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
tiptapEditor.on('alert', ({ message }) => { eventHub.$on('alert', ({ message }) => {
expect(message).toBe('An error occurred while uploading the file. Please try again.'); expect(message).toBe('An error occurred while uploading the file. Please try again.');
done(); done();
}); });
......
...@@ -4,19 +4,21 @@ import { ...@@ -4,19 +4,21 @@ import {
LOADING_ERROR_EVENT, LOADING_ERROR_EVENT,
} from '~/content_editor/constants'; } from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor'; import { ContentEditor } from '~/content_editor/services/content_editor';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor } from '../test_utils'; import { createTestEditor } from '../test_utils';
describe('content_editor/services/content_editor', () => { describe('content_editor/services/content_editor', () => {
let contentEditor; let contentEditor;
let serializer; let serializer;
let eventHub;
beforeEach(() => { beforeEach(() => {
const tiptapEditor = createTestEditor(); const tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'destroy'); jest.spyOn(tiptapEditor, 'destroy');
serializer = { deserialize: jest.fn() }; serializer = { deserialize: jest.fn() };
contentEditor = new ContentEditor({ tiptapEditor, serializer }); eventHub = eventHubFactory();
contentEditor = new ContentEditor({ tiptapEditor, serializer, eventHub });
}); });
describe('.dispose', () => { describe('.dispose', () => {
...@@ -34,13 +36,13 @@ describe('content_editor/services/content_editor', () => { ...@@ -34,13 +36,13 @@ describe('content_editor/services/content_editor', () => {
serializer.deserialize.mockResolvedValueOnce(''); serializer.deserialize.mockResolvedValueOnce('');
}); });
it('emits loadingContent and loadingSuccess event', () => { it('emits loadingContent and loadingSuccess event in the eventHub', () => {
let loadingContentEmitted = false; let loadingContentEmitted = false;
contentEditor.on(LOADING_CONTENT_EVENT, () => { eventHub.$on(LOADING_CONTENT_EVENT, () => {
loadingContentEmitted = true; loadingContentEmitted = true;
}); });
contentEditor.on(LOADING_SUCCESS_EVENT, () => { eventHub.$on(LOADING_SUCCESS_EVENT, () => {
expect(loadingContentEmitted).toBe(true); expect(loadingContentEmitted).toBe(true);
}); });
...@@ -56,7 +58,7 @@ describe('content_editor/services/content_editor', () => { ...@@ -56,7 +58,7 @@ describe('content_editor/services/content_editor', () => {
}); });
it('emits loadingError event', async () => { it('emits loadingError event', async () => {
contentEditor.on(LOADING_ERROR_EVENT, (e) => { eventHub.$on(LOADING_ERROR_EVENT, (e) => {
expect(e).toBe('error'); expect(e).toBe('error');
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment