Commit a1713d26 authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Paul Slaughter

Refactor: Do not include Tiptap Editor class in Vue reactivity

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66540

Also note the [follow up][1] to further improve the reactivity
approach for initializing the Content Editor.

[1]: Move Content Editor loading and initialization to a Vue component
parent 0902a6f8
...@@ -10,6 +10,11 @@ export default { ...@@ -10,6 +10,11 @@ export default {
TiptapEditorContent, TiptapEditorContent,
TopToolbar, TopToolbar,
}, },
provide() {
return {
tiptapEditor: this.contentEditor.tiptapEditor,
};
},
props: { props: {
contentEditor: { contentEditor: {
type: ContentEditor, type: ContentEditor,
...@@ -38,7 +43,7 @@ export default { ...@@ -38,7 +43,7 @@ export default {
class="md-area" class="md-area"
:class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }" :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
> >
<top-toolbar ref="toolbar" class="gl-mb-4" :content-editor="contentEditor" /> <top-toolbar ref="toolbar" class="gl-mb-4" />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" /> <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
</div> </div>
</div> </div>
......
<script>
import { debounce } from 'lodash';
export const tiptapToComponentMap = {
update: 'docUpdate',
selectionUpdate: 'selectionUpdate',
transaction: 'transaction',
};
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
export default {
inject: ['tiptapEditor'],
created() {
this.disposables = [];
Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
const eventHandler = debounce((params) => this.handleTipTapEvent(tiptapEvent, params), 100);
this.tiptapEditor?.on(tiptapEvent, eventHandler);
this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler));
});
},
beforeDestroy() {
this.disposables.forEach((dispose) => dispose());
},
methods: {
handleTipTapEvent(tiptapEvent, params) {
this.$emit(getComponentEventName(tiptapEvent), params);
},
},
render() {
return this.$slots.default;
},
};
</script>
<script> <script>
import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2'; import EditorStateObserver from './editor_state_observer.vue';
export default { export default {
components: { components: {
GlButton, GlButton,
EditorStateObserver,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
}, },
inject: ['tiptapEditor'],
props: { props: {
iconName: { iconName: {
type: String, type: String,
required: true, required: true,
}, },
tiptapEditor: {
type: TiptapEditor,
required: true,
},
contentType: { contentType: {
type: String, type: String,
required: true, required: true,
...@@ -32,12 +30,15 @@ export default { ...@@ -32,12 +30,15 @@ export default {
default: '', default: '',
}, },
}, },
computed: { data() {
isActive() { return {
return this.tiptapEditor.isActive(this.contentType) && this.tiptapEditor.isFocused; isActive: null,
}, };
}, },
methods: { methods: {
updateActive({ editor }) {
this.isActive = editor.isActive(this.contentType) && editor.isFocused;
},
execute() { execute() {
const { contentType } = this; const { contentType } = this;
...@@ -51,6 +52,7 @@ export default { ...@@ -51,6 +52,7 @@ export default {
}; };
</script> </script>
<template> <template>
<editor-state-observer @transaction="updateActive">
<gl-button <gl-button
v-gl-tooltip v-gl-tooltip
category="tertiary" category="tertiary"
...@@ -62,4 +64,5 @@ export default { ...@@ -62,4 +64,5 @@ export default {
:icon="iconName" :icon="iconName"
@click="execute" @click="execute"
/> />
</editor-state-observer>
</template> </template>
...@@ -8,7 +8,6 @@ import { ...@@ -8,7 +8,6 @@ import {
GlDropdownItem, GlDropdownItem,
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { acceptedMimes } from '../extensions/image'; import { acceptedMimes } from '../extensions/image';
import { getImageAlt } from '../services/utils'; import { getImageAlt } from '../services/utils';
...@@ -24,12 +23,7 @@ export default { ...@@ -24,12 +23,7 @@ export default {
directives: { directives: {
GlTooltip, GlTooltip,
}, },
props: { inject: ['tiptapEditor'],
tiptapEditor: {
type: TiptapEditor,
required: true,
},
},
data() { data() {
return { return {
imgSrc: '', imgSrc: '',
......
...@@ -8,9 +8,9 @@ import { ...@@ -8,9 +8,9 @@ import {
GlDropdownItem, GlDropdownItem,
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2';
import Link from '../extensions/link'; import Link from '../extensions/link';
import { hasSelection } from '../services/utils'; import { hasSelection } from '../services/utils';
import EditorStateObserver from './editor_state_observer.vue';
export default { export default {
components: { components: {
...@@ -20,34 +20,25 @@ export default { ...@@ -20,34 +20,25 @@ export default {
GlDropdownDivider, GlDropdownDivider,
GlDropdownItem, GlDropdownItem,
GlButton, GlButton,
EditorStateObserver,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
}, },
props: { inject: ['tiptapEditor'],
tiptapEditor: {
type: TiptapEditor,
required: true,
},
},
data() { data() {
return { return {
linkHref: '', linkHref: '',
isActive: false,
}; };
}, },
computed: { methods: {
isActive() { updateLinkState({ editor }) {
return this.tiptapEditor.isActive(Link.name);
},
},
mounted() {
this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
const { canonicalSrc, href } = editor.getAttributes(Link.name); const { canonicalSrc, href } = editor.getAttributes(Link.name);
this.isActive = editor.isActive(Link.name);
this.linkHref = canonicalSrc || href; this.linkHref = canonicalSrc || href;
});
}, },
methods: {
updateLink() { updateLink() {
this.tiptapEditor this.tiptapEditor
.chain() .chain()
...@@ -78,6 +69,7 @@ export default { ...@@ -78,6 +69,7 @@ export default {
}; };
</script> </script>
<template> <template>
<editor-state-observer @transaction="updateLinkState">
<gl-dropdown <gl-dropdown
v-gl-tooltip v-gl-tooltip
:aria-label="__('Insert link')" :aria-label="__('Insert link')"
...@@ -100,4 +92,5 @@ export default { ...@@ -100,4 +92,5 @@ export default {
{{ __('Remove link') }} {{ __('Remove link') }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</editor-state-observer>
</template> </template>
<script> <script>
import { GlDropdown, GlDropdownDivider, GlDropdownForm, GlButton } from '@gitlab/ui'; import { GlDropdown, GlDropdownDivider, GlDropdownForm, GlButton } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { clamp } from '../services/utils'; import { clamp } from '../services/utils';
...@@ -18,12 +17,7 @@ export default { ...@@ -18,12 +17,7 @@ export default {
GlDropdownForm, GlDropdownForm,
GlButton, GlButton,
}, },
props: { inject: ['tiptapEditor'],
tiptapEditor: {
type: TiptapEditor,
required: true,
},
},
data() { data() {
return { return {
maxRows: MIN_ROWS, maxRows: MIN_ROWS,
......
<script> <script>
import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '../constants'; import { TEXT_STYLE_DROPDOWN_ITEMS } from '../constants';
import EditorStateObserver from './editor_state_observer.vue';
export default { export default {
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
EditorStateObserver,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
}, },
props: { inject: ['tiptapEditor'],
tiptapEditor: { data() {
type: TiptapEditor, return {
required: true, activeItem: null,
}, };
}, },
computed: { computed: {
activeItem() {
return TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
this.tiptapEditor.isActive(item.contentType, item.commandParams),
);
},
activeItemLabel() { activeItemLabel() {
const { activeItem } = this; const { activeItem } = this;
...@@ -31,6 +27,11 @@ export default { ...@@ -31,6 +27,11 @@ export default {
}, },
}, },
methods: { methods: {
updateActiveItem({ editor }) {
this.activeItem = TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
editor.isActive(item.contentType, item.commandParams),
);
},
execute(item) { execute(item) {
const { editorCommand, contentType, commandParams } = item; const { editorCommand, contentType, commandParams } = item;
const value = commandParams?.level; const value = commandParams?.level;
...@@ -38,8 +39,8 @@ export default { ...@@ -38,8 +39,8 @@ export default {
if (editorCommand) { if (editorCommand) {
this.tiptapEditor this.tiptapEditor
.chain() .chain()
.focus()
[editorCommand](commandParams || {}) [editorCommand](commandParams || {})
.focus()
.run(); .run();
} }
...@@ -56,6 +57,7 @@ export default { ...@@ -56,6 +57,7 @@ export default {
}; };
</script> </script>
<template> <template>
<editor-state-observer @transaction="updateActiveItem">
<gl-dropdown <gl-dropdown
v-gl-tooltip="$options.i18n.placeholder" v-gl-tooltip="$options.i18n.placeholder"
size="small" size="small"
...@@ -72,4 +74,5 @@ export default { ...@@ -72,4 +74,5 @@ export default {
{{ item.label }} {{ item.label }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</editor-state-observer>
</template> </template>
<script> <script>
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants'; import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
import { ContentEditor } from '../services/content_editor';
import Divider from './divider.vue'; import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue'; import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue'; import ToolbarImageButton from './toolbar_image_button.vue';
...@@ -23,12 +22,6 @@ export default { ...@@ -23,12 +22,6 @@ export default {
Divider, Divider,
}, },
mixins: [trackingMixin], mixins: [trackingMixin],
props: {
contentEditor: {
type: ContentEditor,
required: true,
},
},
methods: { methods: {
trackToolbarControlExecution({ contentType: property, value }) { trackToolbarControlExecution({ contentType: property, value }) {
this.track(TOOLBAR_CONTROL_TRACKING_ACTION, { this.track(TOOLBAR_CONTROL_TRACKING_ACTION, {
...@@ -45,7 +38,6 @@ export default { ...@@ -45,7 +38,6 @@ export default {
> >
<toolbar-text-style-dropdown <toolbar-text-style-dropdown
data-testid="text-styles" data-testid="text-styles"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<divider /> <divider />
...@@ -55,7 +47,6 @@ export default { ...@@ -55,7 +47,6 @@ export default {
icon-name="bold" icon-name="bold"
editor-command="toggleBold" editor-command="toggleBold"
:label="__('Bold text')" :label="__('Bold text')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -64,7 +55,6 @@ export default { ...@@ -64,7 +55,6 @@ export default {
icon-name="italic" icon-name="italic"
editor-command="toggleItalic" editor-command="toggleItalic"
:label="__('Italic text')" :label="__('Italic text')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -73,7 +63,6 @@ export default { ...@@ -73,7 +63,6 @@ export default {
icon-name="strikethrough" icon-name="strikethrough"
editor-command="toggleStrike" editor-command="toggleStrike"
:label="__('Strikethrough')" :label="__('Strikethrough')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -82,19 +71,13 @@ export default { ...@@ -82,19 +71,13 @@ export default {
icon-name="code" icon-name="code"
editor-command="toggleCode" editor-command="toggleCode"
:label="__('Code')" :label="__('Code')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-link-button
data-testid="link"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-link-button data-testid="link" @execute="trackToolbarControlExecution" />
<divider /> <divider />
<toolbar-image-button <toolbar-image-button
ref="imageButton" ref="imageButton"
data-testid="image" data-testid="image"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -103,7 +86,6 @@ export default { ...@@ -103,7 +86,6 @@ export default {
icon-name="quote" icon-name="quote"
editor-command="toggleBlockquote" editor-command="toggleBlockquote"
:label="__('Insert a quote')" :label="__('Insert a quote')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -112,7 +94,6 @@ export default { ...@@ -112,7 +94,6 @@ export default {
icon-name="doc-code" icon-name="doc-code"
editor-command="toggleCodeBlock" editor-command="toggleCodeBlock"
:label="__('Insert a code block')" :label="__('Insert a code block')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -121,7 +102,6 @@ export default { ...@@ -121,7 +102,6 @@ export default {
icon-name="list-bulleted" icon-name="list-bulleted"
editor-command="toggleBulletList" editor-command="toggleBulletList"
:label="__('Add a bullet list')" :label="__('Add a bullet list')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -130,7 +110,6 @@ export default { ...@@ -130,7 +110,6 @@ export default {
icon-name="list-numbered" icon-name="list-numbered"
editor-command="toggleOrderedList" editor-command="toggleOrderedList"
:label="__('Add a numbered list')" :label="__('Add a numbered list')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-button <toolbar-button
...@@ -139,13 +118,9 @@ export default { ...@@ -139,13 +118,9 @@ export default {
icon-name="dash" icon-name="dash"
editor-command="setHorizontalRule" editor-command="setHorizontalRule"
:label="__('Add a horizontal rule')" :label="__('Add a horizontal rule')"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-table-button
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution" @execute="trackToolbarControlExecution"
/> />
<toolbar-table-button @execute="trackToolbarControlExecution" />
</div> </div>
</template> </template>
<style> <style>
......
...@@ -134,7 +134,6 @@ export default { ...@@ -134,7 +134,6 @@ export default {
isContentEditorLoading: true, isContentEditorLoading: true,
useContentEditor: false, useContentEditor: false,
commitMessage: '', commitMessage: '',
contentEditor: null,
isDirty: false, isDirty: false,
contentEditorRenderFailed: false, contentEditorRenderFailed: false,
}; };
......
...@@ -38,10 +38,10 @@ describe('ContentEditor', () => { ...@@ -38,10 +38,10 @@ describe('ContentEditor', () => {
expect(editorContent.classes()).toContain('md'); expect(editorContent.classes()).toContain('md');
}); });
it('renders top toolbar component and attaches editor instance', () => { it('renders top toolbar component', () => {
createWrapper(editor); createWrapper(editor);
expect(wrapper.findComponent(TopToolbar).props().contentEditor).toBe(editor); expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
}); });
it.each` it.each`
......
import { shallowMount } from '@vue/test-utils';
import { each } from 'lodash';
import EditorStateObserver, {
tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => {
let tiptapEditor;
let wrapper;
let onDocUpdateListener;
let onSelectionUpdateListener;
let onTransactionListener;
const buildEditor = () => {
tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'on');
};
const buildWrapper = () => {
wrapper = shallowMount(EditorStateObserver, {
provide: { tiptapEditor },
listeners: {
docUpdate: onDocUpdateListener,
selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener,
},
});
};
beforeEach(() => {
onDocUpdateListener = jest.fn();
onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn();
buildEditor();
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
});
describe('when editor content changes', () => {
it('emits update, selectionUpdate, and transaction events', () => {
const content = '<p>My paragraph</p>';
tiptapEditor.commands.insertContent(content);
expect(onDocUpdateListener).toHaveBeenCalledWith(
expect.objectContaining({ editor: tiptapEditor }),
);
expect(onSelectionUpdateListener).toHaveBeenCalledWith(
expect.objectContaining({ editor: tiptapEditor }),
);
expect(onSelectionUpdateListener).toHaveBeenCalledWith(
expect.objectContaining({ editor: tiptapEditor }),
);
});
});
describe('when component is destroyed', () => {
it('removes onTiptapDocUpdate and onTiptapSelectionUpdate hooks', () => {
jest.spyOn(tiptapEditor, 'off');
wrapper.destroy();
each(tiptapToComponentMap, (_, tiptapEvent) => {
expect(tiptapEditor.off).toHaveBeenCalledWith(
tiptapEvent,
tiptapEditor.on.mock.calls.find(([eventName]) => eventName === tiptapEvent)[1],
);
});
});
});
});
import { GlButton } from '@gitlab/ui'; 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 ToolbarButton from '~/content_editor/components/toolbar_button.vue'; import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
import { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_button', () => { describe('content_editor/components/toolbar_button', () => {
let wrapper; let wrapper;
...@@ -20,9 +21,12 @@ describe('content_editor/components/toolbar_button', () => { ...@@ -20,9 +21,12 @@ describe('content_editor/components/toolbar_button', () => {
wrapper = shallowMount(ToolbarButton, { wrapper = shallowMount(ToolbarButton, {
stubs: { stubs: {
GlButton, GlButton,
EditorStateObserver,
}, },
propsData: { provide: {
tiptapEditor, tiptapEditor,
},
propsData: {
contentType: CONTENT_TYPE, contentType: CONTENT_TYPE,
iconName: ICON_NAME, iconName: ICON_NAME,
label: LABEL, label: LABEL,
...@@ -51,14 +55,20 @@ describe('content_editor/components/toolbar_button', () => { ...@@ -51,14 +55,20 @@ describe('content_editor/components/toolbar_button', () => {
${{ isActive: true, isFocused: true }} | ${'button is active'} | ${true} ${{ isActive: true, isFocused: true }} | ${'button is active'} | ${true}
${{ isActive: false, isFocused: true }} | ${'button is not active'} | ${false} ${{ isActive: false, isFocused: true }} | ${'button is not active'} | ${false}
${{ isActive: true, isFocused: false }} | ${'button is not active '} | ${false} ${{ isActive: true, isFocused: false }} | ${'button is not active '} | ${false}
`('$outcomeDescription when when editor state is $editorState', ({ editorState, outcome }) => { `(
'$outcomeDescription when when editor state is $editorState',
async ({ editorState, outcome }) => {
tiptapEditor.isActive.mockReturnValueOnce(editorState.isActive); tiptapEditor.isActive.mockReturnValueOnce(editorState.isActive);
tiptapEditor.isFocused = editorState.isFocused; tiptapEditor.isFocused = editorState.isFocused;
buildWrapper(); buildWrapper();
await emitEditorEvent({ event: 'transaction', tiptapEditor });
expect(findButton().classes().includes('active')).toBe(outcome); expect(findButton().classes().includes('active')).toBe(outcome);
expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE); expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE);
}); },
);
describe('when button is clicked', () => { describe('when button is clicked', () => {
it('executes the content type command when executeCommand = true', async () => { it('executes the content type command when executeCommand = true', async () => {
......
...@@ -10,7 +10,7 @@ describe('content_editor/components/toolbar_image_button', () => { ...@@ -10,7 +10,7 @@ describe('content_editor/components/toolbar_image_button', () => {
const buildWrapper = () => { const buildWrapper = () => {
wrapper = mountExtended(ToolbarImageButton, { wrapper = mountExtended(ToolbarImageButton, {
propsData: { provide: {
tiptapEditor: editor, tiptapEditor: editor,
}, },
}); });
......
...@@ -3,7 +3,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; ...@@ -3,7 +3,7 @@ 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 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 } from '../test_utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
jest.mock('~/content_editor/services/utils'); jest.mock('~/content_editor/services/utils');
...@@ -13,7 +13,7 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -13,7 +13,7 @@ describe('content_editor/components/toolbar_link_button', () => {
const buildWrapper = () => { const buildWrapper = () => {
wrapper = mountExtended(ToolbarLinkButton, { wrapper = mountExtended(ToolbarLinkButton, {
propsData: { provide: {
tiptapEditor: editor, tiptapEditor: editor,
}, },
}); });
...@@ -43,6 +43,8 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -43,6 +43,8 @@ describe('content_editor/components/toolbar_link_button', () => {
beforeEach(async () => { beforeEach(async () => {
jest.spyOn(editor, 'isActive').mockReturnValueOnce(true); jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
buildWrapper(); buildWrapper();
await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
}); });
it('sets dropdown as active when link extension is active', () => { it('sets dropdown as active when link extension is active', () => {
...@@ -88,7 +90,7 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -88,7 +90,7 @@ describe('content_editor/components/toolbar_link_button', () => {
href: '/username/my-project/uploads/abcdefgh133535/my-file.zip', href: '/username/my-project/uploads/abcdefgh133535/my-file.zip',
}); });
await editor.emit('selectionUpdate', { editor }); await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
expect(findLinkURLInput().element.value).toEqual('uploads/my-file.zip'); expect(findLinkURLInput().element.value).toEqual('uploads/my-file.zip');
}); });
...@@ -98,7 +100,7 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -98,7 +100,7 @@ describe('content_editor/components/toolbar_link_button', () => {
href: 'https://gitlab.com', href: 'https://gitlab.com',
}); });
await editor.emit('selectionUpdate', { editor }); await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
expect(findLinkURLInput().element.value).toEqual('https://gitlab.com'); expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
}); });
......
...@@ -9,7 +9,7 @@ describe('content_editor/components/toolbar_table_button', () => { ...@@ -9,7 +9,7 @@ describe('content_editor/components/toolbar_table_button', () => {
const buildWrapper = () => { const buildWrapper = () => {
wrapper = mountExtended(ToolbarTableButton, { wrapper = mountExtended(ToolbarTableButton, {
propsData: { provide: {
tiptapEditor: editor, tiptapEditor: editor,
}, },
}); });
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
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 { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_headings_dropdown', () => { describe('content_editor/components/toolbar_text_style_dropdown', () => {
let wrapper; let wrapper;
let tiptapEditor; let tiptapEditor;
...@@ -22,9 +23,12 @@ describe('content_editor/components/toolbar_headings_dropdown', () => { ...@@ -22,9 +23,12 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
stubs: { stubs: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
EditorStateObserver,
}, },
propsData: { provide: {
tiptapEditor, tiptapEditor,
},
propsData: {
...propsData, ...propsData,
}, },
}); });
...@@ -50,7 +54,7 @@ describe('content_editor/components/toolbar_headings_dropdown', () => { ...@@ -50,7 +54,7 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
describe('when there is an active item ', () => { describe('when there is an active item ', () => {
let activeTextStyle; let activeTextStyle;
beforeEach(() => { beforeEach(async () => {
[, activeTextStyle] = TEXT_STYLE_DROPDOWN_ITEMS; [, activeTextStyle] = TEXT_STYLE_DROPDOWN_ITEMS;
tiptapEditor.isActive.mockImplementation( tiptapEditor.isActive.mockImplementation(
...@@ -59,6 +63,7 @@ describe('content_editor/components/toolbar_headings_dropdown', () => { ...@@ -59,6 +63,7 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
); );
buildWrapper(); buildWrapper();
await emitEditorEvent({ event: 'transaction', tiptapEditor });
}); });
it('displays the active text style label as the dropdown toggle text ', () => { it('displays the active text style label as the dropdown toggle text ', () => {
...@@ -79,9 +84,10 @@ describe('content_editor/components/toolbar_headings_dropdown', () => { ...@@ -79,9 +84,10 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
}); });
describe('when there isn’t an active item', () => { describe('when there isn’t an active item', () => {
beforeEach(() => { beforeEach(async () => {
tiptapEditor.isActive.mockReturnValue(false); tiptapEditor.isActive.mockReturnValue(false);
buildWrapper(); buildWrapper();
await emitEditorEvent({ event: 'transaction', tiptapEditor });
}); });
it('sets dropdown as disabled', () => { it('sets dropdown as disabled', () => {
......
...@@ -6,34 +6,19 @@ import { ...@@ -6,34 +6,19 @@ import {
TOOLBAR_CONTROL_TRACKING_ACTION, TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL, CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants'; } from '~/content_editor/constants';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
describe('content_editor/components/top_toolbar', () => { describe('content_editor/components/top_toolbar', () => {
let wrapper; let wrapper;
let contentEditor;
let trackingSpy; let trackingSpy;
const buildEditor = () => {
contentEditor = createContentEditor({ renderMarkdown: () => true });
};
const buildWrapper = () => { const buildWrapper = () => {
wrapper = extendedWrapper( wrapper = extendedWrapper(shallowMount(TopToolbar));
shallowMount(TopToolbar, {
propsData: {
contentEditor,
},
}),
);
}; };
beforeEach(() => { beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn); trackingSpy = mockTracking(undefined, null, jest.spyOn);
}); });
beforeEach(() => {
buildEditor();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -60,7 +45,6 @@ describe('content_editor/components/top_toolbar', () => { ...@@ -60,7 +45,6 @@ describe('content_editor/components/top_toolbar', () => {
it('renders the toolbar control with the provided properties', () => { it('renders the toolbar control with the provided properties', () => {
expect(wrapper.findByTestId(testId).props()).toEqual({ expect(wrapper.findByTestId(testId).props()).toEqual({
...controlProps, ...controlProps,
tiptapEditor: contentEditor.tiptapEditor,
}); });
}); });
......
...@@ -4,6 +4,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'; ...@@ -4,6 +4,7 @@ import { Paragraph } from '@tiptap/extension-paragraph';
import { Text } from '@tiptap/extension-text'; import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2'; import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder'; import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
export const createDocBuilder = ({ tiptapEditor, names = {} }) => { export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
const docBuilders = builders(tiptapEditor.schema, { const docBuilders = builders(tiptapEditor.schema, {
...@@ -14,6 +15,12 @@ export const createDocBuilder = ({ tiptapEditor, names = {} }) => { ...@@ -14,6 +15,12 @@ export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
return { eq, builders: docBuilders }; return { eq, builders: docBuilders };
}; };
export const emitEditorEvent = ({ tiptapEditor, event, params = {} }) => {
tiptapEditor.emit(event, { editor: tiptapEditor, ...params });
return nextTick();
};
/** /**
* Creates an instance of the Tiptap Editor class * Creates an instance of the Tiptap Editor class
* with a minimal configuration for testing purposes. * with a minimal configuration for testing purposes.
......
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