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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/content_editor/components')
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap4
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_spec.js (renamed from spec/frontend/content_editor/components/code_block_bubble_menu_spec.js)36
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_spec.js (renamed from spec/frontend/content_editor/components/formatting_bubble_menu_spec.js)19
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_spec.js227
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_spec.js234
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js2
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js (renamed from spec/frontend/content_editor/components/wrappers/frontmatter_spec.js)33
-rw-r--r--spec/frontend/content_editor/components/wrappers/media_spec.js69
9 files changed, 528 insertions, 98 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 e508cddd6f9..a63cca006da 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-button btn-default-tertiary btn-icon\\">
+exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = `
+"<b-button-stub size=\\"md\\" 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/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
index 074c311495f..3a15ea45f40 100644
--- a/spec/frontend/content_editor/components/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
@@ -1,14 +1,14 @@
import { BubbleMenu } from '@tiptap/vue-2';
-import { GlButton, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import Vue from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import CodeBlockBubbleMenu from '~/content_editor/components/code_block_bubble_menu.vue';
+import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
-import { createTestEditor, emitEditorEvent } from '../test_utils';
+import { createTestEditor, emitEditorEvent } from '../../test_utils';
-describe('content_editor/components/code_block_bubble_menu', () => {
+describe('content_editor/components/bubble_menus/code_block', () => {
let wrapper;
let tiptapEditor;
let bubbleMenu;
@@ -52,7 +52,7 @@ describe('content_editor/components/code_block_bubble_menu', () => {
await emitEditorEvent({ event: 'transaction', tiptapEditor });
expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
it('selects plaintext language by default', async () => {
@@ -82,12 +82,26 @@ describe('content_editor/components/code_block_bubble_menu', () => {
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (nomnoml)');
});
- it('delete button deletes the code block', async () => {
- tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+ describe('copy button', () => {
+ it('copies the text of the code block', async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = Math.PI / 2;</pre>');
+
+ await wrapper.findByTestId('copy-code-block').vm.$emit('click');
- await wrapper.findComponent(GlButton).vm.$emit('click');
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('var a = Math.PI / 2;');
+ });
+ });
- expect(tiptapEditor.getText()).toBe('');
+ describe('delete button', () => {
+ it('deletes the code block', async () => {
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+
+ await wrapper.findByTestId('delete-code-block').vm.$emit('click');
+
+ expect(tiptapEditor.getText()).toBe('');
+ });
});
describe('when opened and search is changed', () => {
@@ -110,7 +124,7 @@ describe('content_editor/components/code_block_bubble_menu', () => {
describe('when dropdown item is clicked', () => {
beforeEach(async () => {
- jest.spyOn(codeBlockLanguageLoader, 'loadLanguages').mockResolvedValue();
+ jest.spyOn(codeBlockLanguageLoader, 'loadLanguage').mockResolvedValue();
findDropdownItems().at(1).vm.$emit('click');
@@ -118,7 +132,7 @@ describe('content_editor/components/code_block_bubble_menu', () => {
});
it('loads language', () => {
- expect(codeBlockLanguageLoader.loadLanguages).toHaveBeenCalledWith(['java']);
+ expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith('java');
});
it('sets code block', () => {
diff --git a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js
index 192ddee78c6..6479c0ba008 100644
--- a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js
@@ -1,15 +1,15 @@
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 FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue';
import {
BUBBLE_MENU_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-import { createTestEditor } from '../test_utils';
+import { createTestEditor } from '../../test_utils';
-describe('content_editor/components/formatting_bubble_menu', () => {
+describe('content_editor/components/bubble_menus/formatting', () => {
let wrapper;
let trackingSpy;
let tiptapEditor;
@@ -42,15 +42,16 @@ describe('content_editor/components/formatting_bubble_menu', () => {
const bubbleMenu = wrapper.findComponent(BubbleMenu);
expect(bubbleMenu.props().editor).toBe(tiptapEditor);
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
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' }}
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold', size: 'medium', category: 'tertiary' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic', size: 'medium', category: 'tertiary' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike', size: 'medium', category: 'tertiary' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode', size: 'medium', category: 'tertiary' }}
+ ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'toggleLink', editorCommandParams: { href: '' }, size: 'medium', category: 'tertiary' }}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
@@ -60,7 +61,7 @@ describe('content_editor/components/formatting_bubble_menu', () => {
expect(wrapper.findByTestId(testId).exists()).toBe(true);
Object.keys(controlProps).forEach((propName) => {
- expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]);
+ expect(wrapper.findByTestId(testId).props(propName)).toEqual(controlProps[propName]);
});
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
new file mode 100644
index 00000000000..ba6d8da9584
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
@@ -0,0 +1,227 @@
+import { GlLink, GlForm } from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Link from '~/content_editor/extensions/link';
+import { createTestEditor, emitEditorEvent } from '../../test_utils';
+
+const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
+
+describe('content_editor/components/bubble_menus/link', () => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let bubbleMenu;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Link] });
+ contentEditor = { resolveUrl: jest.fn() };
+ eventHub = eventHubFactory();
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(LinkBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ eventHub,
+ },
+ });
+ };
+
+ const expectLinkButtonsToExist = (exist = true) => {
+ expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
+ expect(wrapper.findByTestId('copy-link-url').exists()).toBe(exist);
+ expect(wrapper.findByTestId('edit-link').exists()).toBe(exist);
+ expect(wrapper.findByTestId('remove-link').exists()).toBe(exist);
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ buildWrapper();
+
+ tiptapEditor
+ .chain()
+ .insertContent(
+ 'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf" title="Click here to download">PDF File</a>',
+ )
+ .setTextSelection(14) // put cursor in the middle of the link
+ .run();
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', async () => {
+ expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ });
+
+ it('shows a clickable link to the URL in the link node', async () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: '/path/to/project/-/wikis/uploads/my_file.pdf',
+ 'aria-label': 'uploads/my_file.pdf',
+ title: 'uploads/my_file.pdf',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('uploads/my_file.pdf');
+ });
+
+ describe('copy button', () => {
+ it('copies the canonical link to clipboard', async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ await wrapper.findByTestId('copy-link-url').vm.$emit('click');
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('uploads/my_file.pdf');
+ });
+ });
+
+ describe('remove link button', () => {
+ it('removes the link', async () => {
+ await wrapper.findByTestId('remove-link').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>Download PDF File</p>');
+ });
+ });
+
+ describe('for a placeholder link', () => {
+ beforeEach(async () => {
+ tiptapEditor
+ .chain()
+ .clearContent()
+ .insertContent('Dummy link')
+ .selectAll()
+ .setLink({ href: '' })
+ .setTextSelection(4)
+ .run();
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ });
+
+ it('directly opens the edit form for a placeholder link', async () => {
+ expectLinkButtonsToExist(false);
+
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+ });
+
+ it('removes the link on clicking apply (if no change)', async () => {
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+
+ expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
+ });
+
+ it('removes the link on clicking cancel', async () => {
+ await wrapper.findByTestId('cancel-link').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
+ });
+ });
+
+ describe('edit button', () => {
+ let linkHrefInput;
+ let linkTitleInput;
+
+ beforeEach(async () => {
+ await wrapper.findByTestId('edit-link').vm.$emit('click');
+
+ linkHrefInput = wrapper.findByTestId('link-href');
+ linkTitleInput = wrapper.findByTestId('link-title');
+ });
+
+ it('hides the link and copy/edit/remove link buttons', async () => {
+ expectLinkButtonsToExist(false);
+ });
+
+ it('shows a form to edit the link', () => {
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+
+ expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
+ expect(linkTitleInput.element.value).toBe('Click here to download');
+ });
+
+ it('extends selection to select the entire link', () => {
+ const { from, to } = tiptapEditor.state.selection;
+
+ expect(from).toBe(10);
+ expect(to).toBe(18);
+ });
+
+ it('shows the copy/edit/remove link buttons again if selection changes to another non-link and then back again to a link', async () => {
+ expectLinkButtonsToExist(false);
+
+ tiptapEditor.commands.setTextSelection(3);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ tiptapEditor.commands.setTextSelection(14);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expectLinkButtonsToExist(true);
+ expect(wrapper.findComponent(GlForm).exists()).toBe(false);
+ });
+
+ describe('after making changes in the form and clicking apply', () => {
+ beforeEach(async () => {
+ linkHrefInput.setValue('https://google.com');
+ linkTitleInput.setValue('Search Google');
+
+ contentEditor.resolveUrl.mockResolvedValue('https://google.com');
+
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+ });
+
+ it('updates prosemirror doc with new link', async () => {
+ expect(tiptapEditor.getHTML()).toBe(
+ '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google" canonicalsrc="https://google.com">PDF File</a></p>',
+ );
+ });
+
+ it('updates the link in the bubble menu', () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: 'https://google.com',
+ 'aria-label': 'https://google.com',
+ title: 'https://google.com',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('https://google.com');
+ });
+ });
+
+ describe('after making changes in the form and clicking cancel', () => {
+ beforeEach(async () => {
+ linkHrefInput.setValue('https://google.com');
+ linkTitleInput.setValue('Search Google');
+
+ await wrapper.findByTestId('cancel-link').vm.$emit('click');
+ });
+
+ it('hides the form and shows the copy/edit/remove link buttons', () => {
+ expectLinkButtonsToExist();
+ });
+
+ it('resets the form with old values of the link from prosemirror', async () => {
+ // click edit once again to show the form back
+ await wrapper.findByTestId('edit-link').vm.$emit('click');
+
+ linkHrefInput = wrapper.findByTestId('link-href');
+ linkTitleInput = wrapper.findByTestId('link-title');
+
+ expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
+ expect(linkTitleInput.element.value).toBe('Click here to download');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
new file mode 100644
index 00000000000..8839caea80e
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
@@ -0,0 +1,234 @@
+import { GlLink, GlForm } from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Image from '~/content_editor/extensions/image';
+import Audio from '~/content_editor/extensions/audio';
+import Video from '~/content_editor/extensions/video';
+import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+} from '../../test_constants';
+
+const TIPTAP_IMAGE_HTML = `<p>
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png">
+</p>`;
+
+const TIPTAP_AUDIO_HTML = `<p>
+ <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const TIPTAP_VIDEO_HTML = `<p>
+ <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
+
+describe.each`
+ mediaType | mediaHTML | filePath | mediaOutputHTML
+ ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
+`(
+ 'content_editor/components/bubble_menus/media ($mediaType)',
+ ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let bubbleMenu;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] });
+ contentEditor = { resolveUrl: jest.fn() };
+ eventHub = eventHubFactory();
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(MediaBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ eventHub,
+ },
+ });
+ };
+
+ 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');
+ };
+
+ const expectLinkButtonsToExist = (exist = true) => {
+ expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
+ expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist);
+ expect(wrapper.findByTestId('edit-media').exists()).toBe(exist);
+ expect(wrapper.findByTestId('delete-media').exists()).toBe(exist);
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ buildWrapper();
+
+ tiptapEditor
+ .chain()
+ .insertContent(mediaHTML)
+ .setNodeSelection(4) // select the media
+ .run();
+
+ contentEditor.resolveUrl.mockResolvedValue(`/group1/project1/-/wikis/${filePath}`);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', async () => {
+ expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ });
+
+ it('shows a clickable link to the image', async () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: `/group1/project1/-/wikis/${filePath}`,
+ 'aria-label': filePath,
+ title: filePath,
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe(filePath);
+ });
+
+ describe('copy button', () => {
+ it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ await wrapper.findByTestId('copy-media-src').vm.$emit('click');
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath);
+ });
+ });
+
+ describe(`remove ${mediaType} button`, () => {
+ it(`removes the ${mediaType}`, async () => {
+ await wrapper.findByTestId('delete-media').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
+ });
+ });
+
+ describe(`replace ${mediaType} button`, () => {
+ it('uploads and replaces the selected image when file input changes', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'deleteSelection',
+ 'uploadAttachment',
+ 'run',
+ ]);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await wrapper.findByTestId('replace-media').vm.$emit('click');
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.deleteSelection).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('edit button', () => {
+ let mediaSrcInput;
+ let mediaTitleInput;
+ let mediaAltInput;
+
+ beforeEach(async () => {
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ mediaSrcInput = wrapper.findByTestId('media-src');
+ mediaTitleInput = wrapper.findByTestId('media-title');
+ mediaAltInput = wrapper.findByTestId('media-alt');
+ });
+
+ it('hides the link and copy/edit/remove link buttons', async () => {
+ expectLinkButtonsToExist(false);
+ });
+
+ it(`shows a form to edit the ${mediaType} src/title/alt`, () => {
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+
+ expect(mediaSrcInput.element.value).toBe(filePath);
+ expect(mediaTitleInput.element.value).toBe('');
+ expect(mediaAltInput.element.value).toBe('test-file');
+ });
+
+ describe('after making changes in the form and clicking apply', () => {
+ beforeEach(async () => {
+ mediaSrcInput.setValue('https://gitlab.com/favicon.png');
+ mediaAltInput.setValue('gitlab favicon');
+ mediaTitleInput.setValue('gitlab favicon');
+
+ contentEditor.resolveUrl.mockResolvedValue('https://gitlab.com/favicon.png');
+
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+ });
+
+ it(`updates prosemirror doc with new src to the ${mediaType}`, async () => {
+ expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML);
+ });
+
+ it(`updates the link to the ${mediaType} in the bubble menu`, () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: 'https://gitlab.com/favicon.png',
+ 'aria-label': 'https://gitlab.com/favicon.png',
+ title: 'https://gitlab.com/favicon.png',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('https://gitlab.com/favicon.png');
+ });
+ });
+
+ describe('after making changes in the form and clicking cancel', () => {
+ beforeEach(async () => {
+ mediaSrcInput.setValue('https://gitlab.com/favicon.png');
+ mediaAltInput.setValue('gitlab favicon');
+ mediaTitleInput.setValue('gitlab favicon');
+
+ await wrapper.findByTestId('cancel-editing-media').vm.$emit('click');
+ });
+
+ it('hides the form and shows the copy/edit/remove link buttons', () => {
+ expectLinkButtonsToExist();
+ });
+
+ it(`resets the form with old values of the ${mediaType} from prosemirror`, async () => {
+ // click edit once again to show the form back
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ mediaSrcInput = wrapper.findByTestId('media-src');
+ mediaTitleInput = wrapper.findByTestId('media-title');
+ mediaAltInput = wrapper.findByTestId('media-alt');
+
+ expect(mediaSrcInput.element.value).toBe(filePath);
+ expect(mediaAltInput.element.value).toBe('test-file');
+ expect(mediaTitleInput.element.value).toBe('');
+ });
+ });
+ });
+ },
+);
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 73fcfeab8bc..9ee3b017831 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -4,7 +4,7 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
+import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import { emitEditorEvent } from '../test_utils';
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index ce50482302d..1f1f7b338c6 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -46,7 +46,7 @@ describe('content_editor/components/toolbar_button', () => {
wrapper.destroy();
});
- it('displays tertiary, small button with a provided label and icon', () => {
+ it('displays tertiary, medium button with a provided label and icon', () => {
buildWrapper();
expect(findButton().html()).toMatchSnapshot();
diff --git a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index 415f1314a36..a564959a3a6 100644
--- a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -1,20 +1,33 @@
+import { nextTick } from 'vue';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { shallowMount } from '@vue/test-utils';
-import FrontmatterWrapper from '~/content_editor/components/wrappers/frontmatter.vue';
+import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
+import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
-describe('content/components/wrappers/frontmatter', () => {
+jest.mock('~/content_editor/services/code_block_language_loader');
+
+describe('content/components/wrappers/code_block', () => {
+ const language = 'yaml';
let wrapper;
+ let updateAttributesFn;
+
+ const createWrapper = async (nodeAttrs = { language }) => {
+ updateAttributesFn = jest.fn();
- const createWrapper = async (nodeAttrs = { language: 'yaml' }) => {
- wrapper = shallowMount(FrontmatterWrapper, {
+ wrapper = shallowMount(CodeBlockWrapper, {
propsData: {
node: {
attrs: nodeAttrs,
},
+ updateAttributes: updateAttributesFn,
},
});
};
+ beforeEach(() => {
+ codeBlockLanguageLoader.findLanguageBySyntax.mockReturnValue({ syntax: language });
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -38,11 +51,21 @@ describe('content/components/wrappers/frontmatter', () => {
});
it('renders label indicating that code block is frontmatter', () => {
- createWrapper();
+ createWrapper({ isFrontmatter: true, language });
const label = wrapper.find('[data-testid="frontmatter-label"]');
expect(label.text()).toEqual('frontmatter:yaml');
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
});
+
+ it('loads code block’s syntax highlight language', async () => {
+ createWrapper();
+
+ expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith(language);
+
+ await nextTick();
+
+ expect(updateAttributesFn).toHaveBeenCalledWith({ language });
+ });
});
diff --git a/spec/frontend/content_editor/components/wrappers/media_spec.js b/spec/frontend/content_editor/components/wrappers/media_spec.js
deleted file mode 100644
index 3e95e2f3914..00000000000
--- a/spec/frontend/content_editor/components/wrappers/media_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MediaWrapper from '~/content_editor/components/wrappers/media.vue';
-
-describe('content/components/wrappers/media', () => {
- let wrapper;
-
- const createWrapper = async (nodeAttrs = {}) => {
- wrapper = shallowMountExtended(MediaWrapper, {
- propsData: {
- node: {
- attrs: nodeAttrs,
- type: {
- name: 'image',
- },
- },
- },
- });
- };
- const findMedia = () => wrapper.findByTestId('media');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a node-view-wrapper with display-inline-block class', () => {
- createWrapper();
-
- expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-display-inline-block');
- });
-
- it('renders an image that displays the node src', () => {
- const src = 'foobar.png';
-
- createWrapper({ src });
-
- expect(findMedia().attributes().src).toBe(src);
- });
-
- describe('when uploading', () => {
- beforeEach(() => {
- createWrapper({ uploading: true });
- });
-
- it('renders a gl-loading-icon component', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('adds gl-opacity-5 class selector to the media tag', () => {
- expect(findMedia().classes()).toContain('gl-opacity-5');
- });
- });
-
- describe('when not uploading', () => {
- beforeEach(() => {
- createWrapper({ uploading: false });
- });
-
- it('does not render a gl-loading-icon component', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('does not add gl-opacity-5 class selector to the media tag', () => {
- expect(findMedia().classes()).not.toContain('gl-opacity-5');
- });
- });
-});