import { GlLink, GlForm } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.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'; const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); describe('content_editor/components/bubble_menus/link_bubble_menu', () => { 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, }, stubs: { BubbleMenu: stubComponent(BubbleMenu), }, }); }; 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 PDF File', ) .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.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('
Download PDF File
'); }); }); 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('Dummy link
'); }); it('removes the link on clicking cancel', async () => { await wrapper.findByTestId('cancel-link').vm.$emit('click'); expect(tiptapEditor.getHTML()).toBe('Dummy link
'); }); }); 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); }); 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( 'Download PDF File
', ); }); 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'); }); }); }); });