Commit 036dd92a authored by Phil Hughes's avatar Phil Hughes

Merge branch 'refactor-event-bus-content-editor' into 'master'

Refactor triggering events in the Content Editor

See merge request gitlab-org/gitlab!81188
parents 119f5983 31977e5d
<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