summaryrefslogtreecommitdiff
path: root/spec/frontend/content_editor/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/content_editor/components')
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap2
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap17
-rw-r--r--spec/frontend/content_editor/components/content_editor_error_spec.js54
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js166
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js75
-rw-r--r--spec/frontend/content_editor/components/formatting_bubble_menu_spec.js80
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js46
-rw-r--r--spec/frontend/content_editor/components/toolbar_image_button_spec.js22
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js52
-rw-r--r--spec/frontend/content_editor/components/toolbar_table_button_spec.js34
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js18
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js35
12 files changed, 472 insertions, 129 deletions
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
index 35c02911e27..e508cddd6f9 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`content_editor/components/toolbar_button displays tertiary, small button with a provided label and icon 1`] = `
-"<b-button-stub size=\\"sm\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-mx-2 gl-button btn-default-tertiary btn-icon\\">
+"<b-button-stub size=\\"sm\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
<!---->
<gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub>
<!---->
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
index e56c37b0dc9..3c88c05a4b4 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -26,8 +26,21 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
</div>
</form>
</li>
- <!---->
- <!---->
+ <li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\">
+ <hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
+ </li>
+ <li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
+ <!---->
+ <!---->
+ <!---->
+ <div class=\\"gl-new-dropdown-item-text-wrapper\\">
+ <p class=\\"gl-new-dropdown-item-text-primary\\">
+ Upload file
+ </p>
+ <!---->
+ </div>
+ <!---->
+ </button></li> <input type=\\"file\\" name=\\"content_editor_attachment\\" class=\\"gl-display-none\\">
</div>
<!---->
</div>
diff --git a/spec/frontend/content_editor/components/content_editor_error_spec.js b/spec/frontend/content_editor/components/content_editor_error_spec.js
new file mode 100644
index 00000000000..8723fb5a338
--- /dev/null
+++ b/spec/frontend/content_editor/components/content_editor_error_spec.js
@@ -0,0 +1,54 @@
+import { GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
+import { createTestEditor, emitEditorEvent } from '../test_utils';
+
+describe('content_editor/components/content_editor_error', () => {
+ let wrapper;
+ let tiptapEditor;
+
+ const findErrorAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = async () => {
+ tiptapEditor = createTestEditor();
+
+ wrapper = shallowMountExtended(ContentEditorError, {
+ provide: {
+ tiptapEditor,
+ },
+ stubs: {
+ EditorStateObserver,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders error when content editor emits an error event', async () => {
+ const error = 'error message';
+
+ createWrapper();
+
+ await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } });
+
+ expect(findErrorAlert().text()).toBe(error);
+ });
+
+ it('allows dismissing the error', async () => {
+ const error = 'error message';
+
+ createWrapper();
+
+ await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } });
+
+ findErrorAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findErrorAlert().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 563e80e04c1..d516baf6f0f 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,91 +1,175 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
+import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
+import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
-import { createContentEditor } from '~/content_editor/services/create_content_editor';
+import {
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+} from '~/content_editor/constants';
+import { emitEditorEvent } from '../test_utils';
+
+jest.mock('~/emoji');
describe('ContentEditor', () => {
let wrapper;
- let editor;
+ let contentEditor;
+ let renderMarkdown;
+ const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
- const findErrorAlert = () => wrapper.findComponent(GlAlert);
+ const findEditorContent = () => wrapper.findComponent(EditorContent);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ const createWrapper = (propsData = {}) => {
+ renderMarkdown = jest.fn();
- const createWrapper = async (contentEditor) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
- contentEditor,
+ renderMarkdown,
+ uploadsPath,
+ ...propsData,
+ },
+ stubs: {
+ EditorStateObserver,
+ ContentEditorProvider,
+ },
+ listeners: {
+ initialized(editor) {
+ contentEditor = editor;
+ },
},
});
};
- beforeEach(() => {
- editor = createContentEditor({ renderMarkdown: () => true });
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders editor content component and attaches editor instance', () => {
- createWrapper(editor);
+ it('triggers initialized event and provides contentEditor instance as event data', () => {
+ createWrapper();
- const editorContent = wrapper.findComponent(EditorContent);
+ expect(contentEditor).not.toBeFalsy();
+ });
+
+ it('renders EditorContent component and provides tiptapEditor instance', () => {
+ createWrapper();
+
+ const editorContent = findEditorContent();
- expect(editorContent.props().editor).toBe(editor.tiptapEditor);
+ expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor);
expect(editorContent.classes()).toContain('md');
});
- it('renders top toolbar component and attaches editor instance', () => {
- createWrapper(editor);
+ it('renders ContentEditorProvider component', () => {
+ createWrapper();
- expect(wrapper.findComponent(TopToolbar).props().contentEditor).toBe(editor);
+ expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
- it.each`
- isFocused | classes
- ${true} | ${['md-area', 'is-focused']}
- ${false} | ${['md-area']}
- `(
- 'has $classes class selectors when tiptapEditor.isFocused = $isFocused',
- ({ isFocused, classes }) => {
- editor.tiptapEditor.isFocused = isFocused;
- createWrapper(editor);
+ it('renders top toolbar component', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
+ });
- expect(findEditorElement().classes()).toStrictEqual(classes);
- },
- );
+ it('adds is-focused class when focus event is emitted', async () => {
+ createWrapper();
- it('adds isFocused class when tiptapEditor is focused', () => {
- editor.tiptapEditor.isFocused = true;
- createWrapper(editor);
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
expect(findEditorElement().classes()).toContain('is-focused');
});
- describe('displaying error', () => {
- const error = 'Content Editor error';
+ it('removes is-focused class when blur event is emitted', async () => {
+ createWrapper();
+
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' });
+
+ expect(findEditorElement().classes()).not.toContain('is-focused');
+ });
+
+ it('emits change event when document is updated', async () => {
+ createWrapper();
+
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' });
+
+ expect(wrapper.emitted('change')).toEqual([
+ [
+ {
+ empty: contentEditor.empty,
+ },
+ ],
+ ]);
+ });
+
+ it('renders content_editor_error component', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(ContentEditorError).exists()).toBe(true);
+ });
+ describe('when loading content', () => {
beforeEach(async () => {
- createWrapper(editor);
+ createWrapper();
- editor.tiptapEditor.emit('error', error);
+ contentEditor.emit(LOADING_CONTENT_EVENT);
await nextTick();
});
- it('displays error notifications from the tiptap editor', () => {
- expect(findErrorAlert().text()).toBe(error);
+ it('displays loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('allows dismissing an error alert', async () => {
- findErrorAlert().vm.$emit('dismiss');
+ it('hides EditorContent component', () => {
+ expect(findEditorContent().exists()).toBe(false);
+ });
+ });
+
+ describe('when loading content succeeds', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ contentEditor.emit(LOADING_CONTENT_EVENT);
+ await nextTick();
+ contentEditor.emit(LOADING_SUCCESS_EVENT);
+ await nextTick();
+ });
+
+ it('hides loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ it('displays EditorContent component', () => {
+ expect(findEditorContent().exists()).toBe(true);
+ });
+ });
+
+ describe('when loading content fails', () => {
+ const error = 'error';
+
+ beforeEach(async () => {
+ createWrapper();
+
+ contentEditor.emit(LOADING_CONTENT_EVENT);
+ await nextTick();
+ contentEditor.emit(LOADING_ERROR_EVENT, error);
await nextTick();
+ });
+
+ it('hides loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
- expect(findErrorAlert().exists()).toBe(false);
+ it('displays EditorContent component', () => {
+ expect(findEditorContent().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js
new file mode 100644
index 00000000000..5e4bb348e1f
--- /dev/null
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -0,0 +1,75 @@
+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],
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
new file mode 100644
index 00000000000..e44a7fa4ddb
--- /dev/null
+++ b/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
@@ -0,0 +1,80 @@
+import { BubbleMenu } from '@tiptap/vue-2';
+import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
+
+import {
+ BUBBLE_MENU_TRACKING_ACTION,
+ CONTENT_EDITOR_TRACKING_LABEL,
+} from '~/content_editor/constants';
+import { createTestEditor } from '../test_utils';
+
+describe('content_editor/components/top_toolbar', () => {
+ let wrapper;
+ let trackingSpy;
+ let tiptapEditor;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor();
+
+ jest.spyOn(tiptapEditor, 'isActive');
+ };
+
+ const buildWrapper = () => {
+ wrapper = shallowMountExtended(FormattingBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
+ buildEditor();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', () => {
+ buildWrapper();
+ const bubbleMenu = wrapper.findComponent(BubbleMenu);
+
+ expect(bubbleMenu.props().editor).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
+ });
+
+ describe.each`
+ testId | controlProps
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold', size: 'medium', category: 'primary' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic', size: 'medium', category: 'primary' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike', size: 'medium', category: 'primary' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode', size: 'medium', category: 'primary' }}
+ `('given a $testId toolbar control', ({ testId, controlProps }) => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('renders the toolbar control with the provided properties', () => {
+ expect(wrapper.findByTestId(testId).exists()).toBe(true);
+
+ Object.keys(controlProps).forEach((propName) => {
+ expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]);
+ });
+ });
+
+ it('tracks the execution of toolbar controls', () => {
+ const eventData = { contentType: 'italic', value: 1 };
+ const { contentType, value } = eventData;
+
+ wrapper.findByTestId(testId).vm.$emit('execute', eventData);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, BUBBLE_MENU_TRACKING_ACTION, {
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+ property: contentType,
+ value,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index d848adcbff8..60263c46bdd 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -1,7 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
-import { createTestEditor, mockChainedCommands } from '../test_utils';
+import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_button', () => {
let wrapper;
@@ -20,9 +21,12 @@ describe('content_editor/components/toolbar_button', () => {
wrapper = shallowMount(ToolbarButton, {
stubs: {
GlButton,
+ EditorStateObserver,
},
- propsData: {
+ provide: {
tiptapEditor,
+ },
+ propsData: {
contentType: CONTENT_TYPE,
iconName: ICON_NAME,
label: LABEL,
@@ -46,19 +50,43 @@ describe('content_editor/components/toolbar_button', () => {
expect(findButton().html()).toMatchSnapshot();
});
+ it('allows customizing the variant, category, size of the button', () => {
+ const variant = 'danger';
+ const category = 'secondary';
+ const size = 'medium';
+
+ buildWrapper({
+ variant,
+ category,
+ size,
+ });
+
+ expect(findButton().props()).toMatchObject({
+ variant,
+ category,
+ size,
+ });
+ });
+
it.each`
editorState | outcomeDescription | outcome
${{ isActive: true, isFocused: true }} | ${'button is active'} | ${true}
${{ isActive: false, isFocused: true }} | ${'button is not active'} | ${false}
${{ isActive: true, isFocused: false }} | ${'button is not active '} | ${false}
- `('$outcomeDescription when when editor state is $editorState', ({ editorState, outcome }) => {
- tiptapEditor.isActive.mockReturnValueOnce(editorState.isActive);
- tiptapEditor.isFocused = editorState.isFocused;
- buildWrapper();
+ `(
+ '$outcomeDescription when when editor state is $editorState',
+ async ({ editorState, outcome }) => {
+ tiptapEditor.isActive.mockReturnValueOnce(editorState.isActive);
+ tiptapEditor.isFocused = editorState.isFocused;
- expect(findButton().classes().includes('active')).toBe(outcome);
- expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE);
- });
+ buildWrapper();
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expect(findButton().classes().includes('active')).toBe(outcome);
+ expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE);
+ },
+ );
describe('when button is clicked', () => {
it('executes the content type command when executeCommand = true', async () => {
diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
index 701dcf83476..dab7e67d7c5 100644
--- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
@@ -1,7 +1,8 @@
import { GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
-import { configure as configureImageExtension } from '~/content_editor/extensions/image';
+import Attachment from '~/content_editor/extensions/attachment';
+import Image from '~/content_editor/extensions/image';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_image_button', () => {
@@ -10,7 +11,7 @@ describe('content_editor/components/toolbar_image_button', () => {
const buildWrapper = () => {
wrapper = mountExtended(ToolbarImageButton, {
- propsData: {
+ provide: {
tiptapEditor: editor,
},
});
@@ -29,13 +30,14 @@ describe('content_editor/components/toolbar_image_button', () => {
};
beforeEach(() => {
- const { tiptapExtension: Image } = configureImageExtension({
- renderMarkdown: jest.fn(),
- uploadsPath: '/uploads/',
- });
-
editor = createTestEditor({
- extensions: [Image],
+ extensions: [
+ Image,
+ Attachment.configure({
+ renderMarkdown: jest.fn(),
+ uploadsPath: '/uploads/',
+ }),
+ ],
});
buildWrapper();
@@ -64,13 +66,13 @@ describe('content_editor/components/toolbar_image_button', () => {
});
it('uploads the selected image when file input changes', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'uploadImage', 'run']);
+ const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
const file = new File(['foo'], 'foo.png', { type: 'image/png' });
await selectFile(file);
expect(commands.focus).toHaveBeenCalled();
- expect(commands.uploadImage).toHaveBeenCalledWith({ file });
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
index 576a2912f72..0cf488260bd 100644
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -1,9 +1,9 @@
-import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui';
+import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
-import { tiptapExtension as Link } from '~/content_editor/extensions/link';
+import Link from '~/content_editor/extensions/link';
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');
@@ -13,21 +13,26 @@ describe('content_editor/components/toolbar_link_button', () => {
const buildWrapper = () => {
wrapper = mountExtended(ToolbarLinkButton, {
- propsData: {
+ provide: {
tiptapEditor: editor,
},
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
const findApplyLinkButton = () => wrapper.findComponent(GlButton);
const findRemoveLinkButton = () => wrapper.findByText('Remove link');
+ const selectFile = async (file) => {
+ const input = wrapper.find({ ref: 'fileSelector' });
+
+ // override the property definition because `input.files` isn't directly modifyable
+ Object.defineProperty(input.element, 'files', { value: [file], writable: true });
+ await input.trigger('change');
+ };
+
beforeEach(() => {
- editor = createTestEditor({
- extensions: [Link],
- });
+ editor = createTestEditor();
});
afterEach(() => {
@@ -45,14 +50,19 @@ describe('content_editor/components/toolbar_link_button', () => {
beforeEach(async () => {
jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
buildWrapper();
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
});
it('sets dropdown as active when link extension is active', () => {
expect(findDropdown().props('toggleClass')).toEqual({ active: true });
});
+ it('does not display the upload file option', () => {
+ expect(wrapper.findByText('Upload file').exists()).toBe(false);
+ });
+
it('displays a remove link dropdown option', () => {
- expect(findDropdownDivider().exists()).toBe(true);
expect(wrapper.findByText('Remove link').exists()).toBe(true);
});
@@ -90,7 +100,7 @@ describe('content_editor/components/toolbar_link_button', () => {
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');
});
@@ -100,14 +110,14 @@ describe('content_editor/components/toolbar_link_button', () => {
href: 'https://gitlab.com',
});
- await editor.emit('selectionUpdate', { editor });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
});
});
});
- describe('when there is not an active link', () => {
+ describe('when there is no active link', () => {
beforeEach(() => {
jest.spyOn(editor, 'isActive');
editor.isActive.mockReturnValueOnce(false);
@@ -118,8 +128,11 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(findDropdown().props('toggleClass')).toEqual({ active: false });
});
+ it('displays the upload file option', () => {
+ expect(wrapper.findByText('Upload file').exists()).toBe(true);
+ });
+
it('does not display a remove link dropdown option', () => {
- expect(findDropdownDivider().exists()).toBe(false);
expect(wrapper.findByText('Remove link').exists()).toBe(false);
});
@@ -138,6 +151,19 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
});
+
+ it('uploads the selected image when file input changes', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
+ });
});
describe('when the user displays the dropdown', () => {
diff --git a/spec/frontend/content_editor/components/toolbar_table_button_spec.js b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
index 237b2848246..056e5e04e1f 100644
--- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
@@ -1,10 +1,6 @@
import { GlDropdown, GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue';
-import { tiptapExtension as Table } from '~/content_editor/extensions/table';
-import { tiptapExtension as TableCell } from '~/content_editor/extensions/table_cell';
-import { tiptapExtension as TableHeader } from '~/content_editor/extensions/table_header';
-import { tiptapExtension as TableRow } from '~/content_editor/extensions/table_row';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_table_button', () => {
@@ -13,7 +9,7 @@ describe('content_editor/components/toolbar_table_button', () => {
const buildWrapper = () => {
wrapper = mountExtended(ToolbarTableButton, {
- propsData: {
+ provide: {
tiptapEditor: editor,
},
});
@@ -23,9 +19,7 @@ describe('content_editor/components/toolbar_table_button', () => {
const getNumButtons = () => findDropdown().findAllComponents(GlButton).length;
beforeEach(() => {
- editor = createTestEditor({
- extensions: [Table, TableCell, TableRow, TableHeader],
- });
+ editor = createTestEditor();
buildWrapper();
});
@@ -35,17 +29,17 @@ describe('content_editor/components/toolbar_table_button', () => {
wrapper.destroy();
});
- it('renders a grid of 3x3 buttons to create a table', () => {
- expect(getNumButtons()).toBe(9); // 3 x 3
+ it('renders a grid of 5x5 buttons to create a table', () => {
+ expect(getNumButtons()).toBe(25); // 5x5
});
describe.each`
row | col | numButtons | tableSize
- ${1} | ${2} | ${9} | ${'1x2'}
- ${2} | ${2} | ${9} | ${'2x2'}
- ${2} | ${3} | ${12} | ${'2x3'}
- ${3} | ${2} | ${12} | ${'3x2'}
- ${3} | ${3} | ${16} | ${'3x3'}
+ ${3} | ${4} | ${25} | ${'3x4'}
+ ${4} | ${4} | ${25} | ${'4x4'}
+ ${4} | ${5} | ${30} | ${'4x5'}
+ ${5} | ${4} | ${30} | ${'5x4'}
+ ${5} | ${5} | ${36} | ${'5x5'}
`('button($row, $col) in the table creator grid', ({ row, col, numButtons, tableSize }) => {
describe('on mouse over', () => {
beforeEach(async () => {
@@ -56,9 +50,7 @@ describe('content_editor/components/toolbar_table_button', () => {
it('marks all rows and cols before it as active', () => {
const prevRow = Math.max(1, row - 1);
const prevCol = Math.max(1, col - 1);
- expect(wrapper.findByTestId(`table-${prevRow}-${prevCol}`).element).toHaveClass(
- 'gl-bg-blue-50!',
- );
+ expect(wrapper.findByTestId(`table-${prevRow}-${prevCol}`).element).toHaveClass('active');
});
it('shows a help text indicating the size of the table being inserted', () => {
@@ -95,8 +87,8 @@ describe('content_editor/components/toolbar_table_button', () => {
});
});
- it('does not create more buttons than a 8x8 grid', async () => {
- for (let i = 3; i < 8; i += 1) {
+ it('does not create more buttons than a 10x10 grid', async () => {
+ for (let i = 5; i < 10; i += 1) {
expect(getNumButtons()).toBe(i * i);
// eslint-disable-next-line no-await-in-loop
@@ -104,6 +96,6 @@ describe('content_editor/components/toolbar_table_button', () => {
expect(findDropdown().element).toHaveText(`Insert a ${i}x${i} table.`);
}
- expect(getNumButtons()).toBe(64); // 8x8 (and not 9x9)
+ expect(getNumButtons()).toBe(100); // 10x10 (and not 11x11)
});
});
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
index 9a46e27404f..65c1c8c8310 100644
--- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -1,11 +1,12 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
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 { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
-import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
-import { createTestEditor, mockChainedCommands } from '../test_utils';
+import Heading from '~/content_editor/extensions/heading';
+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 tiptapEditor;
@@ -22,9 +23,12 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
stubs: {
GlDropdown,
GlDropdownItem,
+ EditorStateObserver,
},
- propsData: {
+ provide: {
tiptapEditor,
+ },
+ propsData: {
...propsData,
},
});
@@ -50,7 +54,7 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
describe('when there is an active item ', () => {
let activeTextStyle;
- beforeEach(() => {
+ beforeEach(async () => {
[, activeTextStyle] = TEXT_STYLE_DROPDOWN_ITEMS;
tiptapEditor.isActive.mockImplementation(
@@ -59,6 +63,7 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
);
buildWrapper();
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
});
it('displays the active text style label as the dropdown toggle text ', () => {
@@ -79,9 +84,10 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
});
describe('when there isn’t an active item', () => {
- beforeEach(() => {
+ beforeEach(async () => {
tiptapEditor.isActive.mockReturnValue(false);
buildWrapper();
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
});
it('sets dropdown as disabled', () => {
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index 5411793cd5e..a5df3d73289 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -1,39 +1,23 @@
-import { shallowMount } from '@vue/test-utils';
import { mockTracking } from 'helpers/tracking_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-import { createContentEditor } from '~/content_editor/services/create_content_editor';
describe('content_editor/components/top_toolbar', () => {
let wrapper;
- let contentEditor;
let trackingSpy;
- const buildEditor = () => {
- contentEditor = createContentEditor({ renderMarkdown: () => true });
- };
const buildWrapper = () => {
- wrapper = extendedWrapper(
- shallowMount(TopToolbar, {
- propsData: {
- contentEditor,
- },
- }),
- );
+ wrapper = shallowMountExtended(TopToolbar);
};
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
- beforeEach(() => {
- buildEditor();
- });
-
afterEach(() => {
wrapper.destroy();
});
@@ -58,18 +42,17 @@ describe('content_editor/components/top_toolbar', () => {
});
it('renders the toolbar control with the provided properties', () => {
- expect(wrapper.findByTestId(testId).props()).toEqual({
- ...controlProps,
- tiptapEditor: contentEditor.tiptapEditor,
+ expect(wrapper.findByTestId(testId).exists()).toBe(true);
+
+ Object.keys(controlProps).forEach((propName) => {
+ expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]);
});
});
- it.each`
- eventData
- ${{ contentType: 'bold' }}
- ${{ contentType: 'blockquote', value: 1 }}
- `('tracks the execution of toolbar controls', ({ eventData }) => {
+ it('tracks the execution of toolbar controls', () => {
+ const eventData = { contentType: 'blockquote', value: 1 };
const { contentType, value } = eventData;
+
wrapper.findByTestId(testId).vm.$emit('execute', eventData);
expect(trackingSpy).toHaveBeenCalledWith(undefined, TOOLBAR_CONTROL_TRACKING_ACTION, {