diff options
Diffstat (limited to 'spec/frontend/content_editor/components')
4 files changed, 356 insertions, 8 deletions
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index ae52cb05eaf..c1c2a125515 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -13,6 +13,7 @@ import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubb import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; import waitForPromises from 'helpers/wait_for_promises'; +import { KEYDOWN_EVENT } from '~/content_editor/constants'; jest.mock('~/emoji'); @@ -26,12 +27,13 @@ describe('ContentEditor', () => { const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator); const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert); - const createWrapper = ({ markdown } = {}) => { + const createWrapper = ({ markdown, autofocus } = {}) => { wrapper = shallowMountExtended(ContentEditor, { propsData: { renderMarkdown, uploadsPath, markdown, + autofocus, }, stubs: { EditorStateObserver, @@ -70,14 +72,22 @@ describe('ContentEditor', () => { expect(editorContent.classes()).toContain('md'); }); - it('renders ContentEditorProvider component', async () => { - await createWrapper(); + it('allows setting the tiptap editor to autofocus', async () => { + createWrapper({ autofocus: 'start' }); + + await nextTick(); + + expect(findEditorContent().props().editor.options.autofocus).toBe('start'); + }); + + it('renders ContentEditorProvider component', () => { + createWrapper(); expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true); }); - it('renders top toolbar component', async () => { - await createWrapper(); + it('renders top toolbar component', () => { + createWrapper(); expect(wrapper.findComponent(TopToolbar).exists()).toBe(true); }); @@ -213,6 +223,17 @@ describe('ContentEditor', () => { }); }); + describe('when editorStateObserver emits keydown event', () => { + it('bubbles up event', () => { + const event = new Event('keydown'); + + createWrapper(); + + findEditorStateObserver().vm.$emit(KEYDOWN_EVENT, event); + expect(wrapper.emitted(KEYDOWN_EVENT)).toEqual([[event]]); + }); + }); + it.each` name | component ${'formatting'} | ${FormattingBubbleMenu} diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js index e8c2d8c8793..9b42f61c98c 100644 --- a/spec/frontend/content_editor/components/editor_state_observer_spec.js +++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js @@ -4,7 +4,7 @@ import EditorStateObserver, { tiptapToComponentMap, } from '~/content_editor/components/editor_state_observer.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; -import { ALERT_EVENT } from '~/content_editor/constants'; +import { ALERT_EVENT, KEYDOWN_EVENT } from '~/content_editor/constants'; import { createTestEditor } from '../test_utils'; describe('content_editor/components/editor_state_observer', () => { @@ -14,6 +14,7 @@ describe('content_editor/components/editor_state_observer', () => { let onSelectionUpdateListener; let onTransactionListener; let onAlertListener; + let onKeydownListener; let eventHub; const buildEditor = () => { @@ -30,6 +31,7 @@ describe('content_editor/components/editor_state_observer', () => { selectionUpdate: onSelectionUpdateListener, transaction: onTransactionListener, [ALERT_EVENT]: onAlertListener, + [KEYDOWN_EVENT]: onKeydownListener, }, }); }; @@ -39,6 +41,7 @@ describe('content_editor/components/editor_state_observer', () => { onSelectionUpdateListener = jest.fn(); onTransactionListener = jest.fn(); onAlertListener = jest.fn(); + onKeydownListener = jest.fn(); buildEditor(); }); @@ -67,8 +70,9 @@ describe('content_editor/components/editor_state_observer', () => { }); it.each` - event | listener - ${ALERT_EVENT} | ${() => onAlertListener} + event | listener + ${ALERT_EVENT} | ${() => onAlertListener} + ${KEYDOWN_EVENT} | ${() => onKeydownListener} `('listens to $event event in the eventBus object', ({ event, listener }) => { const args = {}; @@ -97,6 +101,7 @@ describe('content_editor/components/editor_state_observer', () => { it.each` event ${ALERT_EVENT} + ${KEYDOWN_EVENT} `('removes $event event hook from eventHub', ({ event }) => { jest.spyOn(eventHub, '$off'); jest.spyOn(eventHub, '$on'); diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js new file mode 100644 index 00000000000..e72eb892e74 --- /dev/null +++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js @@ -0,0 +1,286 @@ +import { GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue'; + +describe('~/content_editor/components/suggestions_dropdown', () => { + let wrapper; + + const buildWrapper = ({ propsData } = {}) => { + wrapper = extendedWrapper( + shallowMount(SuggestionsDropdown, { + propsData: { + nodeType: 'reference', + command: jest.fn(), + ...propsData, + }, + }), + ); + }; + + const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' }; + const exampleIssue = { iid: 123, title: 'Test Issue' }; + const exampleMergeRequest = { iid: 224, title: 'Test MR' }; + const exampleMilestone1 = { iid: 21, title: '13' }; + const exampleMilestone2 = { iid: 24, title: 'Milestone with spaces' }; + + const exampleCommand = { + name: 'due', + description: 'Set due date', + params: ['<in 2 days | this Friday | December 31st>'], + }; + const exampleEpic = { + iid: 8884, + title: '❓ Remote Development | Solution validation', + reference: 'gitlab-org&8884', + }; + const exampleLabel1 = { + title: 'Create', + color: '#E44D2A', + type: 'GroupLabel', + textColor: '#FFFFFF', + }; + const exampleLabel2 = { + title: 'Weekly Team Announcement', + color: '#E44D2A', + type: 'GroupLabel', + textColor: '#FFFFFF', + }; + const exampleLabel3 = { + title: 'devops::create', + color: '#E44D2A', + type: 'GroupLabel', + textColor: '#FFFFFF', + }; + const exampleVulnerability = { + id: 60850147, + title: 'System procs network activity', + }; + const exampleSnippet = { + id: 2420859, + title: 'Project creation QueryRecorder logs', + }; + const exampleEmoji = { + c: 'people', + e: '😃', + d: 'smiling face with open mouth', + u: '6.0', + name: 'smiley', + }; + + const insertedEmojiProps = { + name: 'smiley', + title: 'smiling face with open mouth', + moji: '😃', + unicodeVersion: '6.0', + }; + + describe('on item select', () => { + it.each` + nodeType | referenceType | char | reference | insertedText | insertedProps + ${'reference'} | ${'user'} | ${'@'} | ${exampleUser} | ${`@root`} | ${{}} + ${'reference'} | ${'issue'} | ${'#'} | ${exampleIssue} | ${`#123`} | ${{}} + ${'reference'} | ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${`!224`} | ${{}} + ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${`%13`} | ${{}} + ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone2} | ${`%Milestone with spaces`} | ${{ originalText: '%"Milestone with spaces"' }} + ${'reference'} | ${'command'} | ${'/'} | ${exampleCommand} | ${'/due'} | ${{}} + ${'reference'} | ${'epic'} | ${'&'} | ${exampleEpic} | ${`gitlab-org&8884`} | ${{}} + ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel1} | ${`Create`} | ${{}} + ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel2} | ${`Weekly Team Announcement`} | ${{ originalText: '~"Weekly Team Announcement"' }} + ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel3} | ${`devops::create`} | ${{ originalText: '~"devops::create"', text: 'devops::create' }} + ${'reference'} | ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability} | ${`[vulnerability:60850147]`} | ${{}} + ${'reference'} | ${'snippet'} | ${'$'} | ${exampleSnippet} | ${`$2420859`} | ${{}} + ${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps} + `( + 'runs a command to insert the selected $referenceType', + ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => { + const commandSpy = jest.fn(); + + buildWrapper({ + propsData: { + char, + command: commandSpy, + nodeType, + nodeProps: { + referenceType, + test: 'prop', + }, + items: [reference], + }, + }); + + wrapper.findComponent(GlDropdownItem).vm.$emit('click'); + + expect(commandSpy).toHaveBeenCalledWith( + expect.objectContaining({ + text: insertedText, + test: 'prop', + ...insertedProps, + }), + ); + }, + ); + }); + + describe('rendering user references', () => { + it('displays avatar labeled component', () => { + buildWrapper({ + propsData: { + char: '@', + nodeProps: { + referenceType: 'user', + }, + items: [exampleUser], + }, + }); + + expect(wrapper.findComponent(GlAvatarLabeled).attributes()).toEqual( + expect.objectContaining({ + label: exampleUser.username, + shape: 'circle', + src: exampleUser.avatar_url, + }), + ); + }); + + describe.each` + referenceType | char | reference | displaysID + ${'issue'} | ${'#'} | ${exampleIssue} | ${true} + ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${true} + ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${false} + `('rendering $referenceType references', ({ referenceType, char, reference, displaysID }) => { + it(`displays ${referenceType} ID and title`, () => { + buildWrapper({ + propsData: { + char, + nodeType: 'reference', + nodeProps: { + referenceType, + }, + items: [reference], + }, + }); + + if (displaysID) expect(wrapper.text()).toContain(`${reference.iid}`); + else expect(wrapper.text()).not.toContain(`${reference.iid}`); + expect(wrapper.text()).toContain(`${reference.title}`); + }); + }); + + describe.each` + referenceType | char | reference + ${'snippet'} | ${'$'} | ${exampleSnippet} + ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability} + `('rendering $referenceType references', ({ referenceType, char, reference }) => { + it(`displays ${referenceType} ID and title`, () => { + buildWrapper({ + propsData: { + char, + nodeProps: { + referenceType, + }, + items: [reference], + }, + }); + + expect(wrapper.text()).toContain(`${reference.id}`); + expect(wrapper.text()).toContain(`${reference.title}`); + }); + }); + + describe('rendering label references', () => { + it.each` + label | displayedTitle | displayedColor + ${exampleLabel1} | ${'Create'} | ${'rgb(228, 77, 42)' /* #E44D2A */} + ${exampleLabel2} | ${'Weekly Team Announcement'} | ${'rgb(228, 77, 42)' /* #E44D2A */} + ${exampleLabel3} | ${'devops::create'} | ${'rgb(228, 77, 42)' /* #E44D2A */} + `('displays label title and color', ({ label, displayedTitle, displayedColor }) => { + buildWrapper({ + propsData: { + char: '~', + nodeProps: { + referenceType: 'label', + }, + items: [label], + }, + }); + + expect(wrapper.text()).toContain(displayedTitle); + expect(wrapper.text()).not.toContain('"'); // no quotes in the dropdown list + expect(wrapper.findByTestId('label-color-box').attributes().style).toEqual( + `background-color: ${displayedColor};`, + ); + }); + }); + + describe('rendering epic references', () => { + it('displays epic title and reference', () => { + buildWrapper({ + propsData: { + char: '&', + nodeProps: { + referenceType: 'epic', + }, + items: [exampleEpic], + }, + }); + + expect(wrapper.text()).toContain(`${exampleEpic.reference}`); + expect(wrapper.text()).toContain(`${exampleEpic.title}`); + }); + }); + + describe('rendering a command (quick action)', () => { + it('displays command name with a slash', () => { + buildWrapper({ + propsData: { + char: '/', + nodeProps: { + referenceType: 'command', + }, + items: [exampleCommand], + }, + }); + + expect(wrapper.text()).toContain(`${exampleCommand.name} `); + }); + }); + + describe('rendering emoji references', () => { + it('displays emoji', () => { + const testEmojis = [ + { + c: 'people', + e: '😄', + d: 'smiling face with open mouth and smiling eyes', + u: '6.0', + name: 'smile', + }, + { + c: 'people', + e: '😸', + d: 'grinning cat face with smiling eyes', + u: '6.0', + name: 'smile_cat', + }, + { c: 'people', e: '😃', d: 'smiling face with open mouth', u: '6.0', name: 'smiley' }, + ]; + + buildWrapper({ + propsData: { + char: ':', + nodeType: 'emoji', + nodeProps: {}, + items: testEmojis, + }, + }); + + testEmojis.forEach((testEmoji) => { + expect(wrapper.text()).toContain(testEmoji.e); + expect(wrapper.text()).toContain(testEmoji.d); + expect(wrapper.text()).toContain(testEmoji.name); + }); + }); + }); + }); +}); diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/label_spec.js new file mode 100644 index 00000000000..9e58669b0ea --- /dev/null +++ b/spec/frontend/content_editor/components/wrappers/label_spec.js @@ -0,0 +1,36 @@ +import { GlLabel } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import LabelWrapper from '~/content_editor/components/wrappers/label.vue'; + +describe('content/components/wrappers/label', () => { + let wrapper; + + const createWrapper = async (node = {}) => { + wrapper = shallowMountExtended(LabelWrapper, { + propsData: { node }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it("renders a GlLabel with the node's text and color", () => { + createWrapper({ attrs: { color: '#ff0000', text: 'foo bar', originalText: '~"foo bar"' } }); + + const glLabel = wrapper.findComponent(GlLabel); + + expect(glLabel.props()).toMatchObject( + expect.objectContaining({ + title: 'foo bar', + backgroundColor: '#ff0000', + }), + ); + }); + + it('renders a scoped label if there is a "::" in the label', () => { + createWrapper({ attrs: { color: '#ff0000', text: 'foo::bar', originalText: '~"foo::bar"' } }); + + expect(wrapper.findComponent(GlLabel).props().scoped).toBe(true); + }); +}); |