diff options
Diffstat (limited to 'spec/frontend/content_editor/components/bubble_menus')
-rw-r--r-- | spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js | 126 | ||||
-rw-r--r-- | spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/code_block_spec.js) | 8 | ||||
-rw-r--r-- | spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/formatting_spec.js) | 11 | ||||
-rw-r--r-- | spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/link_spec.js) | 136 | ||||
-rw-r--r-- | spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js (renamed from spec/frontend/content_editor/components/bubble_menus/media_spec.js) | 13 |
5 files changed, 252 insertions, 42 deletions
diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js new file mode 100644 index 00000000000..0700cf5d529 --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js @@ -0,0 +1,126 @@ +import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; +import { createTestEditor } from '../../test_utils'; + +jest.mock('@tiptap/extension-bubble-menu'); + +describe('content_editor/components/bubble_menus/bubble_menu', () => { + let wrapper; + let tiptapEditor; + const pluginKey = 'key'; + const shouldShow = jest.fn(); + const tippyOptions = { placement: 'bottom' }; + const pluginInitializationResult = {}; + + const buildEditor = () => { + tiptapEditor = createTestEditor(); + }; + + const createWrapper = (propsData = {}) => { + wrapper = shallowMountExtended(BubbleMenu, { + provide: { + tiptapEditor, + }, + propsData: { + pluginKey, + shouldShow, + tippyOptions, + ...propsData, + }, + slots: { + default: '<div>menu content</div>', + }, + }); + }; + + const setupMocks = () => { + BubbleMenuPlugin.mockReturnValueOnce(pluginInitializationResult); + jest.spyOn(tiptapEditor, 'registerPlugin').mockImplementationOnce(() => true); + }; + + const invokeTippyEvent = (eventName, eventArgs) => { + const pluginConfig = BubbleMenuPlugin.mock.calls[0][0]; + + pluginConfig.tippyOptions[eventName](eventArgs); + }; + + beforeEach(() => { + buildEditor(); + setupMocks(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('initializes BubbleMenuPlugin', async () => { + createWrapper({}); + + await nextTick(); + + expect(BubbleMenuPlugin).toHaveBeenCalledWith({ + pluginKey, + editor: tiptapEditor, + shouldShow, + element: wrapper.vm.$el, + tippyOptions: expect.objectContaining({ + onHidden: expect.any(Function), + onShow: expect.any(Function), + ...tippyOptions, + }), + }); + + expect(tiptapEditor.registerPlugin).toHaveBeenCalledWith(pluginInitializationResult); + }); + + it('does not render default slot by default', async () => { + createWrapper({}); + + await nextTick(); + + expect(wrapper.text()).not.toContain('menu content'); + }); + + describe('when onShow event handler is invoked', () => { + const onShowArgs = {}; + + beforeEach(async () => { + createWrapper({}); + + await nextTick(); + + invokeTippyEvent('onShow', onShowArgs); + }); + + it('displays the menu content', () => { + expect(wrapper.text()).toContain('menu content'); + }); + + it('emits show event', () => { + expect(wrapper.emitted('show')).toEqual([[onShowArgs]]); + }); + }); + + describe('when onHidden event handler is invoked', () => { + const onHiddenArgs = {}; + + beforeEach(async () => { + createWrapper({}); + + await nextTick(); + + invokeTippyEvent('onShow', onHiddenArgs); + invokeTippyEvent('onHidden', onHiddenArgs); + }); + + it('displays the menu content', () => { + expect(wrapper.text()).not.toContain('menu content'); + }); + + it('emits show event', () => { + expect(wrapper.emitted('hidden')).toEqual([[onHiddenArgs]]); + }); + }); +}); diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js index 154035a46ed..378b11f4ae9 100644 --- a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js @@ -1,4 +1,3 @@ -import { BubbleMenu } from '@tiptap/vue-2'; import { GlDropdown, GlDropdownForm, @@ -9,8 +8,9 @@ import { import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; -import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue'; +import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; +import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; import Diagram from '~/content_editor/extensions/diagram'; import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; @@ -18,7 +18,7 @@ import { createTestEditor, emitEditorEvent } from '../../test_utils'; const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); -describe('content_editor/components/bubble_menus/code_block', () => { +describe('content_editor/components/bubble_menus/code_block_bubble_menu', () => { let wrapper; let tiptapEditor; let contentEditor; @@ -40,6 +40,7 @@ describe('content_editor/components/bubble_menus/code_block', () => { }, stubs: { GlDropdownItem: stubComponent(GlDropdownItem), + BubbleMenu: stubComponent(BubbleMenu), }, }); }; @@ -73,7 +74,6 @@ describe('content_editor/components/bubble_menus/code_block', () => { await emitEditorEvent({ event: 'transaction', tiptapEditor }); - expect(bubbleMenu.props('editor')).toBe(tiptapEditor); expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); }); diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js index 1e2f58d9e40..cce17176129 100644 --- a/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js @@ -1,7 +1,8 @@ -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/bubble_menus/formatting.vue'; +import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting_bubble_menu.vue'; +import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; +import { stubComponent } from 'helpers/stub_component'; import { BUBBLE_MENU_TRACKING_ACTION, @@ -9,7 +10,7 @@ import { } from '~/content_editor/constants'; import { createTestEditor } from '../../test_utils'; -describe('content_editor/components/bubble_menus/formatting', () => { +describe('content_editor/components/bubble_menus/formatting_bubble_menu', () => { let wrapper; let trackingSpy; let tiptapEditor; @@ -25,6 +26,9 @@ describe('content_editor/components/bubble_menus/formatting', () => { provide: { tiptapEditor, }, + stubs: { + BubbleMenu: stubComponent(BubbleMenu), + }, }); }; @@ -41,7 +45,6 @@ describe('content_editor/components/bubble_menus/formatting', () => { buildWrapper(); const bubbleMenu = wrapper.findComponent(BubbleMenu); - expect(bubbleMenu.props().editor).toBe(tiptapEditor); expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); }); diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js index 93204deb68c..9aa9c6483f4 100644 --- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js @@ -1,18 +1,20 @@ import { GlLink, GlForm } from '@gitlab/ui'; -import { BubbleMenu } from '@tiptap/vue-2'; +import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue'; +import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue'; +import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; +import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; +import { stubComponent } from 'helpers/stub_component'; import Link from '~/content_editor/extensions/link'; -import { createTestEditor, emitEditorEvent } from '../../test_utils'; +import { createTestEditor } from '../../test_utils'; const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); -describe('content_editor/components/bubble_menus/link', () => { +describe('content_editor/components/bubble_menus/link_bubble_menu', () => { let wrapper; let tiptapEditor; let contentEditor; - let bubbleMenu; let eventHub; const buildEditor = () => { @@ -28,9 +30,28 @@ describe('content_editor/components/bubble_menus/link', () => { contentEditor, eventHub, }, + stubs: { + BubbleMenu: stubComponent(BubbleMenu), + }, }); }; + const showMenu = () => { + wrapper.findComponent(BubbleMenu).vm.$emit('show'); + return nextTick(); + }; + + const buildWrapperAndDisplayMenu = () => { + buildWrapper(); + + return showMenu(); + }; + + const findBubbleMenu = () => wrapper.findComponent(BubbleMenu); + const findLink = () => wrapper.findComponent(GlLink); + const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); + const findEditLinkButton = () => wrapper.findByTestId('edit-link'); + const expectLinkButtonsToExist = (exist = true) => { expect(wrapper.findComponent(GlLink).exists()).toBe(exist); expect(wrapper.findByTestId('copy-link-url').exists()).toBe(exist); @@ -40,7 +61,6 @@ describe('content_editor/components/bubble_menus/link', () => { beforeEach(async () => { buildEditor(); - buildWrapper(); tiptapEditor .chain() @@ -49,10 +69,6 @@ describe('content_editor/components/bubble_menus/link', () => { ) .setTextSelection(14) // put cursor in the middle of the link .run(); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - bubbleMenu = wrapper.findComponent(BubbleMenu); }); afterEach(() => { @@ -60,13 +76,15 @@ describe('content_editor/components/bubble_menus/link', () => { }); it('renders bubble menu component', async () => { - expect(bubbleMenu.props('editor')).toBe(tiptapEditor); - expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); + await buildWrapperAndDisplayMenu(); + + expect(findBubbleMenu().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( + await buildWrapperAndDisplayMenu(); + + expect(findLink().attributes()).toEqual( expect.objectContaining({ href: '/path/to/project/-/wikis/uploads/my_file.pdf', 'aria-label': 'uploads/my_file.pdf', @@ -74,11 +92,82 @@ describe('content_editor/components/bubble_menus/link', () => { target: '_blank', }), ); - expect(link.text()).toBe('uploads/my_file.pdf'); + expect(findLink().text()).toBe('uploads/my_file.pdf'); + }); + + it('updates the bubble menu state when @selectionUpdate event is triggered', async () => { + const linkUrl = 'https://gitlab.com'; + + await buildWrapperAndDisplayMenu(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: '/path/to/project/-/wikis/uploads/my_file.pdf', + }), + ); + + tiptapEditor + .chain() + .setContent( + `Link to <a href="${linkUrl}" data-canonical-src="${linkUrl}" title="Click here to download">GitLab</a>`, + ) + .setTextSelection(11) + .run(); + + findEditorStateObserver().vm.$emit('selectionUpdate'); + + await nextTick(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: linkUrl, + }), + ); + }); + + describe('when the selection changes within the same link', () => { + it('does not update the bubble menu state', async () => { + await buildWrapperAndDisplayMenu(); + + await findEditLinkButton().trigger('click'); + + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + + tiptapEditor.commands.setTextSelection(13); + + findEditorStateObserver().vm.$emit('selectionUpdate'); + + await nextTick(); + + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + }); + }); + + it('cleans bubble menu state when hidden event is triggered', async () => { + await buildWrapperAndDisplayMenu(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: '/path/to/project/-/wikis/uploads/my_file.pdf', + }), + ); + + findBubbleMenu().vm.$emit('hidden'); + + await nextTick(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: '#', + }), + ); + expect(findLink().text()).toEqual(''); }); describe('copy button', () => { it('copies the canonical link to clipboard', async () => { + await buildWrapperAndDisplayMenu(); + jest.spyOn(navigator.clipboard, 'writeText'); await wrapper.findByTestId('copy-link-url').vm.$emit('click'); @@ -89,6 +178,7 @@ describe('content_editor/components/bubble_menus/link', () => { describe('remove link button', () => { it('removes the link', async () => { + await buildWrapperAndDisplayMenu(); await wrapper.findByTestId('remove-link').vm.$emit('click'); expect(tiptapEditor.getHTML()).toBe('<p>Download PDF File</p>'); @@ -106,7 +196,7 @@ describe('content_editor/components/bubble_menus/link', () => { .setTextSelection(4) .run(); - await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await buildWrapperAndDisplayMenu(); }); it('directly opens the edit form for a placeholder link', async () => { @@ -133,6 +223,7 @@ describe('content_editor/components/bubble_menus/link', () => { let linkTitleInput; beforeEach(async () => { + await buildWrapperAndDisplayMenu(); await wrapper.findByTestId('edit-link').vm.$emit('click'); linkHrefInput = wrapper.findByTestId('link-href'); @@ -157,19 +248,6 @@ describe('content_editor/components/bubble_menus/link', () => { 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'); diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js index fada4f06743..13c6495ac41 100644 --- a/spec/frontend/content_editor/components/bubble_menus/media_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js @@ -1,7 +1,8 @@ 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 BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; +import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue'; +import { stubComponent } from 'helpers/stub_component'; import eventHubFactory from '~/helpers/event_hub_factory'; import Image from '~/content_editor/extensions/image'; import Audio from '~/content_editor/extensions/audio'; @@ -33,7 +34,7 @@ describe.each` ${'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)', + 'content_editor/components/bubble_menus/media_bubble_menu ($mediaType)', ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => { let wrapper; let tiptapEditor; @@ -54,11 +55,14 @@ describe.each` contentEditor, eventHub, }, + stubs: { + BubbleMenu: stubComponent(BubbleMenu), + }, }); }; const selectFile = async (file) => { - const input = wrapper.find({ ref: 'fileSelector' }); + const input = wrapper.findComponent({ ref: 'fileSelector' }); // override the property definition because `input.files` isn't directly modifyable Object.defineProperty(input.element, 'files', { value: [file], writable: true }); @@ -94,7 +98,6 @@ describe.each` }); it('renders bubble menu component', async () => { - expect(bubbleMenu.props('editor')).toBe(tiptapEditor); expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); }); |