diff options
Diffstat (limited to 'spec/frontend/content_editor')
15 files changed, 1894 insertions, 107 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 a708f7d5f47..0fafd42095b 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 @@ -3,7 +3,7 @@ exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = ` <b-button-stub aria-label="Bold" - class="btn-default-tertiary btn-icon gl-button gl-mr-3" + class="btn-default-tertiary btn-icon gl-button gl-mr-2" size="sm" tag="button" title="Bold" diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js index 2a6ab75227c..6e8a6092667 100644 --- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js @@ -80,6 +80,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () => await emitEditorEvent({ event: 'transaction', tiptapEditor }); expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Plain text'); + expect(wrapper.findComponent(GlDropdown).attributes('contenteditable')).toBe(String(false)); }); it('selects appropriate language based on the code block', async () => { diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 816c9458201..bbc0203344c 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -1,6 +1,8 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { EditorContent, Editor } from '@tiptap/vue-2'; import { nextTick } from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContentEditor from '~/content_editor/components/content_editor.vue'; import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; @@ -16,11 +18,10 @@ import waitForPromises from 'helpers/wait_for_promises'; import { KEYDOWN_EVENT } from '~/content_editor/constants'; import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; -jest.mock('~/emoji'); - describe('ContentEditor', () => { let wrapper; let renderMarkdown; + let mock; const uploadsPath = '/uploads'; const findEditorElement = () => wrapper.findByTestId('content-editor'); @@ -32,6 +33,7 @@ describe('ContentEditor', () => { wrapper = shallowMountExtended(ContentEditor, { propsData: { renderMarkdown, + markdownDocsPath: '/docs/markdown', uploadsPath, markdown, autofocus, @@ -49,9 +51,17 @@ describe('ContentEditor', () => { }; beforeEach(() => { + mock = new MockAdapter(axios); + // ignore /-/emojis requests + mock.onGet().reply(200, []); + renderMarkdown = jest.fn(); }); + afterEach(() => { + mock.restore(); + }); + it('triggers initialized event', () => { createWrapper(); diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js index ee3ad59bf9a..b17a1b5fc11 100644 --- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js +++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js @@ -1,5 +1,6 @@ -import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui'; +import { GlAvatar, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue'; @@ -14,11 +15,17 @@ describe('~/content_editor/components/suggestions_dropdown', () => { command: jest.fn(), ...propsData, }, + stubs: ['gl-emoji'], }), ); }; - const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' }; + const exampleUser = { + username: 'root', + avatar_url: 'root_avatar.png', + type: 'User', + name: 'Administrator', + }; const exampleIssue = { iid: 123, title: 'Test Issue' }; const exampleMergeRequest = { iid: 224, title: 'Test MR' }; const exampleMilestone1 = { iid: 21, title: '13' }; @@ -61,11 +68,14 @@ describe('~/content_editor/components/suggestions_dropdown', () => { title: 'Project creation QueryRecorder logs', }; const exampleEmoji = { - c: 'people', - e: '😃', - d: 'smiling face with open mouth', - u: '6.0', - name: 'smiley', + emoji: { + c: 'people', + e: '😃', + d: 'smiling face with open mouth', + u: '6.0', + name: 'smiley', + }, + fieldValue: 'smiley', }; const insertedEmojiProps = { @@ -95,6 +105,68 @@ describe('~/content_editor/components/suggestions_dropdown', () => { expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(loading); }); + it('selects first item if query is not empty and items are available', async () => { + buildWrapper({ + propsData: { + char: '@', + nodeType: 'reference', + nodeProps: { + referenceType: 'member', + }, + items: [exampleUser], + query: 'ro', + }, + }); + + await nextTick(); + + expect( + wrapper.findByTestId('content-editor-suggestions-dropdown').find('li').classes(), + ).toContain('focused'); + }); + + describe('when query is defined', () => { + it.each` + nodeType | referenceType | reference | query | expectedHTML + ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'<strong class="gl-text-body!">r</strong>oot'} + ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'Administ<strong class="gl-text-body!">r</strong>ator'} + ${'reference'} | ${'issue'} | ${exampleIssue} | ${'test'} | ${'<strong class="gl-text-body!">Test</strong> Issue'} + ${'reference'} | ${'issue'} | ${exampleIssue} | ${'12'} | ${'<strong class="gl-text-body!">12</strong>3'} + ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'test'} | ${'<strong class="gl-text-body!">Test</strong> MR'} + ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'22'} | ${'<strong class="gl-text-body!">22</strong>4'} + ${'reference'} | ${'epic'} | ${exampleEpic} | ${'rem'} | ${'❓ <strong class="gl-text-body!">Rem</strong>ote Development | Solution validation'} + ${'reference'} | ${'epic'} | ${exampleEpic} | ${'88'} | ${'gitlab-org&<strong class="gl-text-body!">88</strong>84'} + ${'reference'} | ${'milestone'} | ${exampleMilestone1} | ${'1'} | ${'<strong class="gl-text-body!">1</strong>3'} + ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'<strong class="gl-text-body!">due</strong>'} + ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'Set <strong class="gl-text-body!">due</strong> date'} + ${'reference'} | ${'label'} | ${exampleLabel1} | ${'c'} | ${'<strong class="gl-text-body!">C</strong>reate'} + ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'network'} | ${'System procs <strong class="gl-text-body!">network</strong> activity'} + ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'85'} | ${'60<strong class="gl-text-body!">85</strong>0147'} + ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'project'} | ${'<strong class="gl-text-body!">Project</strong> creation QueryRecorder logs'} + ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'242'} | ${'<strong class="gl-text-body!">242</strong>0859'} + ${'emoji'} | ${'emoji'} | ${exampleEmoji} | ${'sm'} | ${'<strong class="gl-text-body!">sm</strong>iley'} + `( + 'highlights query as bolded in $referenceType text', + ({ nodeType, referenceType, reference, query, expectedHTML }) => { + buildWrapper({ + propsData: { + char: '@', + nodeType, + nodeProps: { + referenceType, + }, + items: [reference], + query, + }, + }); + + expect(wrapper.findByTestId('content-editor-suggestions-dropdown').html()).toContain( + expectedHTML, + ); + }, + ); + }); + describe('on item select', () => { it.each` nodeType | referenceType | char | reference | insertedText | insertedProps @@ -146,7 +218,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => { }); describe('rendering user references', () => { - it('displays avatar labeled component', () => { + it('displays avatar component', () => { buildWrapper({ propsData: { char: '@', @@ -157,13 +229,11 @@ describe('~/content_editor/components/suggestions_dropdown', () => { }, }); - expect(wrapper.findComponent(GlAvatarLabeled).attributes()).toEqual( - expect.objectContaining({ - label: exampleUser.username, - shape: 'circle', - src: exampleUser.avatar_url, - }), - ); + expect(wrapper.findComponent(GlAvatar).attributes()).toMatchObject({ + entityname: exampleUser.username, + shape: 'circle', + src: exampleUser.avatar_url, + }); }); describe.each` @@ -273,20 +343,46 @@ describe('~/content_editor/components/suggestions_dropdown', () => { it('displays emoji', () => { const testEmojis = [ { - c: 'people', - e: '😄', - d: 'smiling face with open mouth and smiling eyes', - u: '6.0', - name: 'smile', + emoji: { + c: 'people', + e: '😄', + d: 'smiling face with open mouth and smiling eyes', + u: '6.0', + name: 'smile', + }, + fieldValue: 'smile', + }, + { + emoji: { + c: 'people', + e: '😸', + d: 'grinning cat face with smiling eyes', + u: '6.0', + name: 'smile_cat', + }, + fieldValue: 'smile_cat', + }, + { + emoji: { + c: 'people', + e: '😃', + d: 'smiling face with open mouth', + u: '6.0', + name: 'smiley', + }, + fieldValue: 'smiley', }, { - c: 'people', - e: '😸', - d: 'grinning cat face with smiling eyes', - u: '6.0', - name: 'smile_cat', + emoji: { + c: 'custom', + e: null, + d: 'party-parrot', + u: 'custom', + name: 'party-parrot', + src: 'https://cultofthepartyparrot.com/parrots/hd/parrot.gif', + }, + fieldValue: 'party-parrot', }, - { c: 'people', e: '😃', d: 'smiling face with open mouth', u: '6.0', name: 'smiley' }, ]; buildWrapper({ @@ -298,11 +394,41 @@ describe('~/content_editor/components/suggestions_dropdown', () => { }, }); - testEmojis.forEach((testEmoji) => { - expect(wrapper.text()).toContain(testEmoji.e); - expect(wrapper.text()).toContain(testEmoji.d); - expect(wrapper.text()).toContain(testEmoji.name); - }); + expect(wrapper.findAllComponents('gl-emoji-stub').at(0).html()).toMatchInlineSnapshot(` + <gl-emoji-stub + data-name="smile" + data-unicode-version="6.0" + title="smiling face with open mouth and smiling eyes" + > + 😄 + </gl-emoji-stub> + `); + expect(wrapper.findAllComponents('gl-emoji-stub').at(1).html()).toMatchInlineSnapshot(` + <gl-emoji-stub + data-name="smile_cat" + data-unicode-version="6.0" + title="grinning cat face with smiling eyes" + > + 😸 + </gl-emoji-stub> + `); + expect(wrapper.findAllComponents('gl-emoji-stub').at(2).html()).toMatchInlineSnapshot(` + <gl-emoji-stub + data-name="smiley" + data-unicode-version="6.0" + title="smiling face with open mouth" + > + 😃 + </gl-emoji-stub> + `); + expect(wrapper.findAllComponents('gl-emoji-stub').at(3).html()).toMatchInlineSnapshot(` + <gl-emoji-stub + data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" + data-name="party-parrot" + data-unicode-version="custom" + title="party-parrot" + /> + `); }); }); }); diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js index 0093393eceb..1f15dc17f7f 100644 --- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js +++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js @@ -97,6 +97,7 @@ describe('content/components/wrappers/code_block', () => { const label = wrapper.findByTestId('frontmatter-label'); expect(label.text()).toEqual('frontmatter:yaml'); + expect(label.attributes('contenteditable')).toBe('false'); expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']); }); @@ -128,6 +129,9 @@ describe('content/components/wrappers/code_block', () => { await nextTick(); expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + expect(wrapper.findByTestId('sandbox-preview').attributes('contenteditable')).toBe( + String(false), + ); jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(false); @@ -214,6 +218,9 @@ describe('content/components/wrappers/code_block', () => { }); it('shows a code suggestion block', () => { + expect(wrapper.findByTestId('code-suggestion-box').attributes('contenteditable')).toBe( + 'false', + ); expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 5'); expect(findCodeDeleted()).toMatchInlineSnapshot(` <code diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js index 275f48ea857..94628f2b2c5 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js @@ -165,6 +165,9 @@ describe('content/components/wrappers/table_cell_base', () => { it('does not allow adding a row before the header', () => { expect(findDropdown().text()).not.toContain('Insert row before'); + expect(wrapper.findByTestId('actions-dropdown').attributes('contenteditable')).toBe( + 'false', + ); }); it('does not allow removing the header row', async () => { diff --git a/spec/frontend/content_editor/extensions/copy_paste_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js index e290b4e5137..6969f4985a1 100644 --- a/spec/frontend/content_editor/extensions/copy_paste_spec.js +++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js @@ -20,12 +20,6 @@ import waitForPromises from 'helpers/wait_for_promises'; import MarkdownSerializer from '~/content_editor/services/markdown_serializer'; import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils'; -const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>'; -const CODE_SUGGESTION_HTML = - '<pre data-lang-params="-0+0" class="js-syntax-highlight language-suggestion" lang="suggestion">Suggested code</pre>'; -const DIAGRAM_HTML = - '<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">'; -const FRONTMATTER_HTML = '<pre lang="yaml" data-lang-params="frontmatter">key: value</pre>'; const PARAGRAPH_HTML = '<p dir="auto">Some text with <strong>bold</strong> and <em>italic</em> text.</p>'; @@ -123,19 +117,6 @@ describe('content_editor/extensions/copy_paste', () => { expect(await triggerPasteEventHandler(buildClipboardEvent({ types, data }))).toBe(true); }); - it.each` - nodeType | html | handled | desc - ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'} - ${'codeSuggestion'} | ${CODE_SUGGESTION_HTML} | ${false} | ${'does not handle'} - ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'} - ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'} - ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'} - `('$desc paste if currently a `$nodeType` is in focus', async ({ html, handled }) => { - tiptapEditor.commands.insertContent(html); - - expect(await triggerPasteEventHandler(buildClipboardEvent())).toBe(handled); - }); - describe.each` eventName | expectedDoc ${'cut'} | ${() => doc(p())} diff --git a/spec/frontend/content_editor/extensions/reference_spec.js b/spec/frontend/content_editor/extensions/reference_spec.js index c25c7c41d75..d4b07d5127e 100644 --- a/spec/frontend/content_editor/extensions/reference_spec.js +++ b/spec/frontend/content_editor/extensions/reference_spec.js @@ -1,9 +1,15 @@ import Reference from '~/content_editor/extensions/reference'; +import ReferenceLabel from '~/content_editor/extensions/reference_label'; import AssetResolver from '~/content_editor/services/asset_resolver'; import { RESOLVED_ISSUE_HTML, RESOLVED_MERGE_REQUEST_HTML, RESOLVED_EPIC_HTML, + RESOLVED_LABEL_HTML, + RESOLVED_SNIPPET_HTML, + RESOLVED_MILESTONE_HTML, + RESOLVED_USER_HTML, + RESOLVED_VULNERABILITY_HTML, } from '../test_constants'; import { createTestEditor, @@ -17,6 +23,7 @@ describe('content_editor/extensions/reference', () => { let doc; let p; let reference; + let referenceLabel; let renderMarkdown; let assetResolver; @@ -25,33 +32,54 @@ describe('content_editor/extensions/reference', () => { assetResolver = new AssetResolver({ renderMarkdown }); tiptapEditor = createTestEditor({ - extensions: [Reference.configure({ assetResolver })], + extensions: [Reference.configure({ assetResolver }), ReferenceLabel], }); ({ - builders: { doc, p, reference }, + builders: { doc, p, reference, referenceLabel }, } = createDocBuilder({ tiptapEditor, names: { reference: { nodeType: Reference.name }, + referenceLabel: { nodeType: ReferenceLabel.name }, }, })); }); describe('when typing a valid reference input rule', () => { - const buildExpectedDoc = (href, originalText, referenceType, text) => + const buildExpectedDoc = (href, originalText, referenceType, text = originalText) => doc(p(reference({ className: null, href, originalText, referenceType, text }), ' ')); + const buildExpectedDocForLabel = (href, originalText, text, color) => + doc( + p( + referenceLabel({ + className: null, + referenceType: 'label', + href, + originalText, + text, + color, + }), + ' ', + ), + ); + it.each` - inputRuleText | mockReferenceHtml | expectedDoc - ${'#1 '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1', 'issue', '#1 (closed)')} - ${'#1+ '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+', 'issue', '500 error on MR approvers edit page (#1 - closed)')} - ${'#1+s '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+s', 'issue', '500 error on MR approvers edit page (#1 - closed) • Unassigned')} - ${'!1 '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1', 'merge_request', '!1 (merged)')} - ${'!1+ '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged)')} - ${'!1+s '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+s', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged) • John Doe')} - ${'&1 '} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1', 'epic', '&1')} - ${'&1+ '} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1+', 'epic', 'Approvals in merge request list (&1)')} + inputRuleText | mockReferenceHtml | expectedDoc + ${'#1'} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1', 'issue', '#1 (closed)')} + ${'#1+'} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+', 'issue', '500 error on MR approvers edit page (#1 - closed)')} + ${'#1+s'} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+s', 'issue', '500 error on MR approvers edit page (#1 - closed) • Unassigned')} + ${'!1'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1', 'merge_request', '!1 (merged)')} + ${'!1+'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged)')} + ${'!1+s'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+s', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged) • John Doe')} + ${'&1'} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1', 'epic', '&1')} + ${'&1+'} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1+', 'epic', 'Approvals in merge request list (&1)')} + ${'@root'} | ${RESOLVED_USER_HTML} | ${() => buildExpectedDoc('/root', '@root', 'user')} + ${'~Aquanix'} | ${RESOLVED_LABEL_HTML} | ${() => buildExpectedDocForLabel('/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix', '~Aquanix', 'Aquanix', 'rgb(230, 84, 49)')} + ${'%v4.0'} | ${RESOLVED_MILESTONE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab-shell/-/milestones/5', '%v4.0', 'milestone')} + ${'$25'} | ${RESOLVED_SNIPPET_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab-shell/-/snippets/25', '$25', 'snippet')} + ${'[vulnerability:1]'} | ${RESOLVED_VULNERABILITY_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab-shell/-/security/vulnerabilities/1', '[vulnerability:1]', 'vulnerability')} `( 'replaces the input rule ($inputRuleText) with a reference node', async ({ inputRuleText, mockReferenceHtml, expectedDoc }) => { @@ -61,8 +89,8 @@ describe('content_editor/extensions/reference', () => { action() { renderMarkdown.mockResolvedValueOnce(mockReferenceHtml); - tiptapEditor.commands.insertContent({ type: 'text', text: inputRuleText }); - triggerNodeInputRule({ tiptapEditor, inputRuleText }); + tiptapEditor.commands.insertContent({ type: 'text', text: `${inputRuleText} ` }); + triggerNodeInputRule({ tiptapEditor, inputRuleText: `${inputRuleText} ` }); }, }); diff --git a/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap b/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap new file mode 100644 index 00000000000..2d16c6b1a2f --- /dev/null +++ b/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap @@ -0,0 +1,256 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataSourceFactory filters items based on command "/assign" for reference type "user" and command 1`] = ` +Array [ + "florida.schoen", + "root", + "all", + "lakeesha.batz", + "laurene_blick", + "myrtis", + "patty", + "Commit451", + "flightjs", + "gitlab-instance-ade037f9", + "gitlab-org", + "gnuwget", + "h5bp", + "jashkenas", + "twitter", +] +`; + +exports[`DataSourceFactory filters items based on command "/assign_reviewer" for reference type "user" and command 1`] = ` +Array [ + "florida.schoen", + "root", + "all", + "errol", + "evelynn_olson", + "Commit451", + "flightjs", + "gitlab-instance-ade037f9", + "gitlab-org", + "gnuwget", + "h5bp", + "jashkenas", + "twitter", +] +`; + +exports[`DataSourceFactory filters items based on command "/label" for reference type "label" and command 1`] = ` +Array [ + "Bronce", + "Contour", + "Corolla", + "Cygsync", + "Frontier", + "Grand Am", + "Onesync", + "Phone", + "Pynefunc", + "Trinix", + "Trounswood", + "group::knowledge", + "scoped label", + "type::one", + "type::two", +] +`; + +exports[`DataSourceFactory filters items based on command "/reassign" for reference type "user" and command 1`] = ` +Array [ + "florida.schoen", + "root", + "all", + "errol", + "evelynn_olson", + "lakeesha.batz", + "laurene_blick", + "myrtis", + "patty", + "Commit451", + "flightjs", + "gitlab-instance-ade037f9", + "gitlab-org", + "gnuwget", + "h5bp", +] +`; + +exports[`DataSourceFactory filters items based on command "/reassign_reviewer" for reference type "user" and command 1`] = ` +Array [ + "florida.schoen", + "root", + "all", + "errol", + "evelynn_olson", + "lakeesha.batz", + "laurene_blick", + "myrtis", + "patty", + "Commit451", + "flightjs", + "gitlab-instance-ade037f9", + "gitlab-org", + "gnuwget", + "h5bp", +] +`; + +exports[`DataSourceFactory filters items based on command "/relabel" for reference type "label" and command 1`] = ` +Array [ + "Amsche", + "Brioffe", + "Bronce", + "Bryncefunc", + "Contour", + "Corolla", + "Cygsync", + "Frontier", + "Ghost", + "Grand Am", + "Onesync", + "Phone", + "Pynefunc", + "Trinix", + "Trounswood", +] +`; + +exports[`DataSourceFactory filters items based on command "/unassign" for reference type "user" and command 1`] = ` +Array [ + "errol", + "evelynn_olson", +] +`; + +exports[`DataSourceFactory filters items based on command "/unassign_reviewer" for reference type "user" and command 1`] = ` +Array [ + "lakeesha.batz", + "laurene_blick", + "myrtis", + "patty", +] +`; + +exports[`DataSourceFactory filters items based on command "/unlabel" for reference type "label" and command 1`] = ` +Array [ + "Amsche", + "Brioffe", + "Bryncefunc", + "Ghost", +] +`; + +exports[`DataSourceFactory for reference type "command", searches for "re" correctly 1`] = ` +Array [ + "relabel", + "remove_milestone", + "remove_estimate", + "remove_time_spent", + "relate", + "remove_epic", + "reassign", + "create_merge_request", +] +`; + +exports[`DataSourceFactory for reference type "epic", searches for "n" correctly 1`] = ` +Array [ + "Nobis quidem aspernatur reprehenderit sunt ut ipsum tempora sapiente sed iste.", + "Minus eius ut omnis quos sunt dicta ex ipsum.", + "Quae nostrum possimus rerum aliquam pariatur a eos aut id.", + "Dicta incidunt vel dignissimos sint sit esse est quibusdam quidem consequatur.", + "Doloremque a quisquam qui culpa numquam doloribus similique iure enim.", +] +`; + +exports[`DataSourceFactory for reference type "issue", searches for "q" correctly 1`] = ` +Array [ + "Quasi id et et nihil sint autem.", + "Eaque omnis eius quas necessitatibus hic ut et corrupti.", + "Aut quisquam magnam eos distinctio incidunt perferendis fugit.", + "Dolorem quisquam cupiditate consequatur perspiciatis sequi eligendi ullam.", + "Nesciunt quia molestiae in aliquam amet et dolorem.", + "Porro tempore qui qui culpa saepe et nam quos.", + "Sed sint a est consequatur quae quasi autem debitis alias.", + "Molestiae minima maxime optio nihil quam eveniet dolor.", + "Et laboriosam aut ratione voluptatem quasi recusandae.", + "Et molestiae delectus voluptates velit vero illo aut rerum quo et.", +] +`; + +exports[`DataSourceFactory for reference type "label", searches for "c" correctly 1`] = ` +Array [ + "Contour", + "Corolla", + "Cygsync", + "scoped label", + "Amsche", + "Bronce", + "Bryncefunc", + "Onesync", + "Pynefunc", +] +`; + +exports[`DataSourceFactory for reference type "merge_request", searches for "n" correctly 1`] = ` +Array [ + "Blanditiis maxime voluptatem ut pariatur vel autem vero non quod libero.", + "Optio nemo qui dolorem sit ipsum qui saepe.", + "Draft: Alunny/publish lib", + "Draft: Fix event current target", + "Draft: Resolve \\"hgvbbvnnb\\"", + "Autem eaque et sed provident enim corrupti molestiae.", + "Always call registry's trigger method from withRegistration", +] +`; + +exports[`DataSourceFactory for reference type "milestone", searches for "16" correctly 1`] = ` +Array [ + "16.7", + "16.8", + "16.9", + "16.10", + "16.11", + "16.0 (expired)", + "16.1 (expired)", + "16.2 (expired)", + "16.3 (expired)", + "16.4 (expired)", + "16.5 (expired)", + "16.6 (expired)", +] +`; + +exports[`DataSourceFactory for reference type "snippet", searches for "s" correctly 1`] = ` +Array [ + "ss", + "test snippet", + "another test snippet", +] +`; + +exports[`DataSourceFactory for reference type "user", searches for "r" correctly 1`] = ` +Array [ + "root", + "errol", + "lakeesha.batz", + "myrtis", + "florida.schoen", + "laurene_blick", + "all", + "twitter", + "gitlab-org", + "evelynn_olson", +] +`; + +exports[`DataSourceFactory for reference type "vulnerability", searches for "cross" correctly 1`] = ` +Array [ + "Cross Site Scripting (Persistent)", + "Cross Site Scripting (Persistent)", + "Cross Site Scripting (Persistent)", +] +`; diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js index 292eec6db77..b0135a6bc9f 100644 --- a/spec/frontend/content_editor/services/asset_resolver_spec.js +++ b/spec/frontend/content_editor/services/asset_resolver_spec.js @@ -3,6 +3,11 @@ import { RESOLVED_ISSUE_HTML, RESOLVED_MERGE_REQUEST_HTML, RESOLVED_EPIC_HTML, + RESOLVED_LABEL_HTML, + RESOLVED_SNIPPET_HTML, + RESOLVED_MILESTONE_HTML, + RESOLVED_USER_HTML, + RESOLVED_VULNERABILITY_HTML, } from '../test_constants'; describe('content_editor/services/asset_resolver', () => { @@ -48,6 +53,32 @@ describe('content_editor/services/asset_resolver', () => { text: '!1 (merged)', }; + const resolvedLabel = { + backgroundColor: 'rgb(230, 84, 49)', + href: '/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix', + text: 'Aquanix', + }; + + const resolvedSnippet = { + href: '/gitlab-org/gitlab-shell/-/snippets/25', + text: '$25', + }; + + const resolvedMilestone = { + href: '/gitlab-org/gitlab-shell/-/milestones/5', + text: '%v4.0', + }; + + const resolvedUser = { + href: '/root', + text: '@root', + }; + + const resolvedVulnerability = { + href: '/gitlab-org/gitlab-shell/-/security/vulnerabilities/1', + text: '[vulnerability:1]', + }; + describe.each` referenceType | referenceId | sentMarkdown | returnedHtml | resolvedReference ${'issue'} | ${'#1'} | ${'#1 #1+ #1+s'} | ${RESOLVED_ISSUE_HTML} | ${resolvedIssue} @@ -59,7 +90,9 @@ describe('content_editor/services/asset_resolver', () => { it(`resolves ${referenceType} reference to href, text, title and summary`, async () => { renderMarkdown.mockResolvedValue(returnedHtml); - expect(await assetResolver.resolveReference(referenceId)).toEqual(resolvedReference); + expect(await assetResolver.resolveReference(referenceId)).toMatchObject( + resolvedReference, + ); }); it.each` @@ -74,6 +107,26 @@ describe('content_editor/services/asset_resolver', () => { }, ); + describe.each` + referenceType | referenceId | returnedHtml | resolvedReference + ${'label'} | ${'~Aquanix'} | ${RESOLVED_LABEL_HTML} | ${resolvedLabel} + ${'snippet'} | ${'$25'} | ${RESOLVED_SNIPPET_HTML} | ${resolvedSnippet} + ${'milestone'} | ${'%v4.0'} | ${RESOLVED_MILESTONE_HTML} | ${resolvedMilestone} + ${'user'} | ${'@root'} | ${RESOLVED_USER_HTML} | ${resolvedUser} + ${'vulnerability'} | ${'[vulnerability:1]'} | ${RESOLVED_VULNERABILITY_HTML} | ${resolvedVulnerability} + `( + 'for reference type $referenceType', + ({ referenceType, referenceId, returnedHtml, resolvedReference }) => { + it(`resolves ${referenceType} reference to href, text and additional props (if any)`, async () => { + renderMarkdown.mockResolvedValue(returnedHtml); + + expect(await assetResolver.resolveReference(referenceId)).toMatchObject( + resolvedReference, + ); + }); + }, + ); + it.each` case | sentMarkdown | returnedHtml ${'no html is returned'} | ${''} | ${''} diff --git a/spec/frontend/content_editor/services/autocomplete_mock_data.js b/spec/frontend/content_editor/services/autocomplete_mock_data.js new file mode 100644 index 00000000000..c1bf2a6ae5b --- /dev/null +++ b/spec/frontend/content_editor/services/autocomplete_mock_data.js @@ -0,0 +1,967 @@ +export const MOCK_MEMBERS = [ + { + type: 'User', + username: 'florida.schoen', + name: 'Anglea Durgan', + avatar_url: + 'https://www.gravatar.com/avatar/ac82b5615d3308ecbcacedad361af8e7?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'User', + username: 'root', + name: 'Administrator', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + availability: null, + }, + { + username: 'all', + name: 'All Project and Group Members', + count: 8, + }, + { + type: 'User', + username: 'errol', + name: "Linnie O'Connell", + avatar_url: + 'https://www.gravatar.com/avatar/d3d9a468a9884eb217fad5ca5b2b9bd7?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'User', + username: 'evelynn_olson', + name: 'Dimple Dare', + avatar_url: + 'https://www.gravatar.com/avatar/bc1e51ee3512c2b4442f51732d655107?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'User', + username: 'lakeesha.batz', + name: 'Larae Veum', + avatar_url: + 'https://www.gravatar.com/avatar/e5605cb9bbb1a28640d65f25f256e541?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'User', + username: 'laurene_blick', + name: 'Evelina Murray', + avatar_url: + 'https://www.gravatar.com/avatar/389768eef61b7b2d125c64ee01c240fb?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'User', + username: 'myrtis', + name: 'Fernanda Adams', + avatar_url: + 'https://www.gravatar.com/avatar/719d5569bd31d4a70e350b4205fa2cb5?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'User', + username: 'patty', + name: 'Emily Toy', + avatar_url: + 'https://www.gravatar.com/avatar/dca2077b662338808459dc11e70d6688?s=80\u0026d=identicon', + availability: null, + }, + { + type: 'Group', + username: 'Commit451', + name: 'Commit451', + avatar_url: null, + count: 5, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'flightjs', + name: 'Flightjs', + avatar_url: null, + count: 5, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'gitlab-instance-ade037f9', + name: 'GitLab Instance', + avatar_url: null, + count: 1, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'gitlab-org', + name: 'Gitlab Org', + avatar_url: null, + count: 5, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'gnuwget', + name: 'Gnuwget', + avatar_url: null, + count: 5, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'h5bp', + name: 'H5bp', + avatar_url: null, + count: 4, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'jashkenas', + name: 'Jashkenas', + avatar_url: null, + count: 5, + mentionsDisabled: null, + }, + { + type: 'Group', + username: 'twitter', + name: 'Twitter', + avatar_url: null, + count: 5, + mentionsDisabled: null, + }, +]; + +export const MOCK_ASSIGNEES = MOCK_MEMBERS.filter( + ({ username }) => username === 'errol' || username === 'evelynn_olson', +); + +export const MOCK_REVIEWERS = MOCK_MEMBERS.filter( + ({ username }) => + username === 'lakeesha.batz' || + username === 'laurene_blick' || + username === 'myrtis' || + username === 'patty', +); + +export const MOCK_ISSUES = [ + { + iid: 31, + title: 'rdfhdfj', + id: null, + }, + { + iid: 30, + title: 'incident1', + id: null, + }, + { + iid: 29, + title: 'example feature rollout', + id: null, + }, + { + iid: 28, + title: 'sagasg', + id: null, + }, + { + iid: 26, + title: 'Quasi id et et nihil sint autem.', + id: null, + }, + { + iid: 25, + title: 'Dolorem quisquam cupiditate consequatur perspiciatis sequi eligendi ullam.', + id: null, + }, + { + iid: 24, + title: 'Et molestiae delectus voluptates velit vero illo aut rerum quo et.', + id: null, + }, + { + iid: 23, + title: 'Nesciunt quia molestiae in aliquam amet et dolorem.', + id: null, + }, + { + iid: 22, + title: 'Sint asperiores unde vel autem delectus ullam dolor nihil et.', + id: null, + }, + { + iid: 21, + title: 'Eaque omnis eius quas necessitatibus hic ut et corrupti.', + id: null, + }, + { + iid: 20, + title: 'Porro tempore qui qui culpa saepe et nam quos.', + id: null, + }, + { + iid: 19, + title: 'Molestiae minima maxime optio nihil quam eveniet dolor.', + id: null, + }, + { + iid: 18, + title: 'Sed sint a est consequatur quae quasi autem debitis alias.', + id: null, + }, + { + iid: 6, + title: 'Et laboriosam aut ratione voluptatem quasi recusandae.', + id: null, + }, + { + iid: 2, + title: 'Aut quisquam magnam eos distinctio incidunt perferendis fugit.', + id: null, + }, +]; + +export const MOCK_EPICS = [ + { + iid: 6, + title: 'sgs', + reference: 'flightjs\u00266', + }, + { + iid: 5, + title: 'Doloremque a quisquam qui culpa numquam doloribus similique iure enim.', + reference: 'flightjs\u00265', + }, + { + iid: 4, + title: 'Minus eius ut omnis quos sunt dicta ex ipsum.', + reference: 'flightjs\u00264', + }, + { + iid: 3, + title: 'Quae nostrum possimus rerum aliquam pariatur a eos aut id.', + reference: 'flightjs\u00263', + }, + { + iid: 2, + title: 'Nobis quidem aspernatur reprehenderit sunt ut ipsum tempora sapiente sed iste.', + reference: 'flightjs\u00262', + }, + { + iid: 1, + title: 'Dicta incidunt vel dignissimos sint sit esse est quibusdam quidem consequatur.', + reference: 'flightjs\u00261', + }, +]; + +export const MOCK_MERGE_REQUESTS = [ + { + iid: 12, + title: "Always call registry's trigger method from withRegistration", + id: null, + }, + { + iid: 11, + title: 'Draft: Alunny/publish lib', + id: null, + }, + { + iid: 10, + title: 'Draft: Resolve "hgvbbvnnb"', + id: null, + }, + { + iid: 9, + title: 'Draft: Fix event current target', + id: null, + }, + { + iid: 3, + title: 'Autem eaque et sed provident enim corrupti molestiae.', + id: null, + }, + { + iid: 2, + title: 'Blanditiis maxime voluptatem ut pariatur vel autem vero non quod libero.', + id: null, + }, + { + iid: 1, + title: 'Optio nemo qui dolorem sit ipsum qui saepe.', + id: null, + }, +]; + +export const MOCK_SNIPPETS = [ + { + id: 24, + title: 'ss', + }, + { + id: 22, + title: 'another test snippet', + }, + { + id: 21, + title: 'test snippet', + }, +]; + +export const MOCK_LABELS = [ + { + title: 'Amsche', + color: '#9964cf', + type: 'GroupLabel', + textColor: '#FFFFFF', + set: true, + }, + { + title: 'Brioffe', + color: '#203e13', + type: 'GroupLabel', + textColor: '#FFFFFF', + set: true, + }, + { + title: 'Bronce', + color: '#c0b7f2', + type: 'GroupLabel', + textColor: '#1F1E24', + }, + { + title: 'Bryncefunc', + color: '#8baa5e', + type: 'GroupLabel', + textColor: '#FFFFFF', + set: true, + }, + { + title: 'Contour', + color: '#8cf3a3', + type: 'ProjectLabel', + textColor: '#1F1E24', + }, + { + title: 'Corolla', + color: '#0384f3', + type: 'ProjectLabel', + textColor: '#FFFFFF', + }, + { + title: 'Cygsync', + color: '#1308c3', + type: 'GroupLabel', + textColor: '#FFFFFF', + }, + { + title: 'Frontier', + color: '#85db43', + type: 'ProjectLabel', + textColor: '#1F1E24', + }, + { + title: 'Ghost', + color: '#df1bc4', + type: 'ProjectLabel', + textColor: '#FFFFFF', + set: true, + }, + { + title: 'Grand Am', + color: '#a1d7ee', + type: 'ProjectLabel', + textColor: '#1F1E24', + }, + { + title: 'Onesync', + color: '#a73ba0', + type: 'GroupLabel', + textColor: '#FFFFFF', + }, + { + title: 'Phone', + color: '#63dceb', + type: 'GroupLabel', + textColor: '#1F1E24', + }, + { + title: 'Pynefunc', + color: '#974b19', + type: 'GroupLabel', + textColor: '#FFFFFF', + }, + { + title: 'Trinix', + color: '#2c894f', + type: 'GroupLabel', + textColor: '#FFFFFF', + }, + { + title: 'Trounswood', + color: '#ad0370', + type: 'GroupLabel', + textColor: '#FFFFFF', + }, + { + title: 'group::knowledge', + color: '#8fbc8f', + type: 'ProjectLabel', + textColor: '#1F1E24', + }, + { + title: 'scoped label', + color: '#6699cc', + type: 'GroupLabel', + textColor: '#FFFFFF', + }, + { + title: 'type::one', + color: '#9400d3', + type: 'ProjectLabel', + textColor: '#FFFFFF', + }, + { + title: 'type::two', + color: '#013220', + type: 'ProjectLabel', + textColor: '#FFFFFF', + }, +]; + +export const MOCK_MILESTONES = [ + { + iid: 65, + title: '15.0', + due_date: '2022-05-17', + id: null, + }, + { + iid: 73, + title: '15.1', + due_date: '2022-06-17', + id: null, + }, + { + iid: 74, + title: '15.2', + due_date: '2022-07-17', + id: null, + }, + { + iid: 75, + title: '15.3', + due_date: '2022-08-17', + id: null, + }, + { + iid: 76, + title: '15.4', + due_date: '2022-09-17', + id: null, + }, + { + iid: 77, + title: '15.5', + due_date: '2022-10-17', + id: null, + }, + { + iid: 81, + title: '15.6', + due_date: '2022-11-17', + id: null, + }, + { + iid: 82, + title: '15.7', + due_date: '2022-12-17', + id: null, + }, + { + iid: 83, + title: '15.8', + due_date: '2023-01-17', + id: null, + }, + { + iid: 84, + title: '15.9', + due_date: '2023-02-17', + id: null, + }, + { + iid: 85, + title: '15.10', + due_date: '2023-03-17', + id: null, + }, + { + iid: 86, + title: '15.11', + due_date: '2023-04-17', + id: null, + }, + { + iid: 80, + title: '16.0', + due_date: '2023-05-17', + id: null, + }, + { + iid: 88, + title: '16.1', + due_date: '2023-06-17', + id: null, + }, + { + iid: 89, + title: '16.2', + due_date: '2023-07-17', + id: null, + }, + { + iid: 90, + title: '16.3', + due_date: '2023-08-17', + id: null, + }, + { + iid: 91, + title: '16.4', + due_date: '2023-09-17', + id: null, + }, + { + iid: 92, + title: '16.5', + due_date: '2023-10-17', + id: null, + }, + { + iid: 93, + title: '16.6', + due_date: '2023-11-10', + id: null, + }, + { + iid: 95, + title: '16.7', + due_date: '2023-12-15', + id: null, + }, + { + iid: 94, + title: '16.8', + due_date: '2024-01-12', + id: null, + }, + { + iid: 96, + title: '16.9', + due_date: '2024-02-09', + id: null, + }, + { + iid: 97, + title: '16.10', + due_date: '2024-03-15', + id: null, + }, + { + iid: 98, + title: '16.11', + due_date: '2024-04-12', + id: null, + }, + { + iid: 87, + title: '17.0', + due_date: '2024-05-10', + id: null, + }, + { + iid: 48, + title: 'Next 1-3 releases', + due_date: null, + id: null, + }, + { + iid: 24, + title: 'Awaiting further demand', + due_date: null, + id: null, + }, + { + iid: 14, + title: 'Backlog', + due_date: null, + id: null, + }, + { + iid: 11, + title: 'Next 4-7 releases', + due_date: null, + id: null, + }, + { + iid: 10, + title: 'Next 3-4 releases', + due_date: null, + id: null, + }, + { + iid: 6, + title: 'Next 7-13 releases', + due_date: null, + id: null, + }, +]; + +export const MOCK_VULNERABILITIES = [ + { + id: 99499903, + title: 'Cross Site Scripting (Persistent)', + }, + { + id: 99495085, + title: 'Possible SQL injection', + }, + { + id: 99490610, + title: 'GitLab Runner Authentication Token', + }, + { + id: 99288920, + title: 'Cross Site Scripting (Persistent)', + }, + { + id: 99258720, + title: 'Cross Site Scripting (Persistent)', + }, +]; + +export const MOCK_COMMANDS = [ + { + name: 'due', + aliases: [], + description: 'Set due date', + warning: '', + icon: '', + params: ['\u003cin 2 days | this Friday | December 31st\u003e'], + }, + { + name: 'duplicate', + aliases: [], + description: 'Mark this issue as a duplicate of another issue', + warning: '', + icon: '', + params: ['#issue'], + }, + { + name: 'clone', + aliases: [], + description: 'Clone this issue', + warning: '', + icon: '', + params: ['path/to/project [--with_notes]'], + }, + { + name: 'move', + aliases: [], + description: 'Move this issue to another project.', + warning: '', + icon: '', + params: ['path/to/project'], + }, + { + name: 'create_merge_request', + aliases: [], + description: 'Create a merge request', + warning: '', + icon: '', + params: ['\u003cbranch name\u003e'], + }, + { + name: 'zoom', + aliases: [], + description: 'Add Zoom meeting', + warning: '', + icon: '', + params: ['\u003cZoom URL\u003e'], + }, + { + name: 'promote_to_incident', + aliases: [], + description: 'Promote issue to incident', + warning: '', + icon: '', + params: [], + }, + { + name: 'close', + aliases: [], + description: 'Close this issue', + warning: '', + icon: '', + params: [], + }, + { + name: 'title', + aliases: [], + description: 'Change title', + warning: '', + icon: '', + params: ['\u003cNew title\u003e'], + }, + { + name: 'label', + aliases: ['labels'], + description: 'Add labels', + warning: '', + icon: '', + params: ['~label1 ~"label 2"'], + }, + { + name: 'unlabel', + aliases: ['remove_label'], + description: 'Remove all or specific labels', + warning: '', + icon: '', + params: ['~label1 ~"label 2"'], + }, + { + name: 'relabel', + aliases: [], + description: 'Replace all labels', + warning: '', + icon: '', + params: ['~label1 ~"label 2"'], + }, + { + name: 'todo', + aliases: [], + description: 'Add a to do', + warning: '', + icon: '', + params: [], + }, + { + name: 'unsubscribe', + aliases: [], + description: 'Unsubscribe', + warning: '', + icon: '', + params: [], + }, + { + name: 'award', + aliases: [], + description: 'Toggle emoji award', + warning: '', + icon: '', + params: [':emoji:'], + }, + { + name: 'shrug', + aliases: [], + description: 'Append the comment with ¯\\_(ツ)_/¯', + warning: '', + icon: '', + params: ['\u003cComment\u003e'], + }, + { + name: 'tableflip', + aliases: [], + description: 'Append the comment with (╯°□°)╯︵ ┻━┻', + warning: '', + icon: '', + params: ['\u003cComment\u003e'], + }, + { + name: 'confidential', + aliases: [], + description: 'Make issue confidential', + warning: '', + icon: '', + params: [], + }, + { + name: 'assign', + aliases: [], + description: 'Assign', + warning: '', + icon: '', + params: ['@user1 @user2'], + }, + { + name: 'unassign', + aliases: [], + description: 'Remove all or specific assignees', + warning: '', + icon: '', + params: ['@user1 @user2'], + }, + { + name: 'milestone', + aliases: [], + description: 'Set milestone', + warning: '', + icon: '', + params: ['%"milestone"'], + }, + { + name: 'remove_milestone', + aliases: [], + description: 'Remove milestone', + warning: '', + icon: '', + params: [], + }, + { + name: 'copy_metadata', + aliases: [], + description: 'Copy labels and milestone from other issue or merge request in this project', + warning: '', + icon: '', + params: ['#issue | !merge_request'], + }, + { + name: 'estimate', + aliases: ['estimate_time'], + description: 'Set time estimate', + warning: '', + icon: '', + params: ['\u003c1w 3d 2h 14m\u003e'], + }, + { + name: 'spend', + aliases: ['spent', 'spend_time'], + description: 'Add or subtract spent time', + warning: '', + icon: '', + params: ['\u003ctime(1h30m | -1h30m)\u003e \u003cdate(YYYY-MM-DD)\u003e'], + }, + { + name: 'remove_estimate', + aliases: ['remove_time_estimate'], + description: 'Remove time estimate', + warning: '', + icon: '', + params: [], + }, + { + name: 'remove_time_spent', + aliases: [], + description: 'Remove spent time', + warning: '', + icon: '', + params: [], + }, + { + name: 'lock', + aliases: [], + description: 'Lock the discussion', + warning: '', + icon: '', + params: [], + }, + { + name: 'cc', + aliases: [], + description: 'CC', + warning: '', + icon: '', + params: ['@user'], + }, + { + name: 'relate', + aliases: [], + description: 'Mark this issue as related to another issue', + warning: '', + icon: '', + params: ['\u003c#issue | group/project#issue | issue URL\u003e'], + }, + { + name: 'unlink', + aliases: [], + description: 'Remove link with another issue', + warning: '', + icon: '', + params: ['\u003c#issue | group/project#issue | issue URL\u003e'], + }, + { + name: 'epic', + aliases: [], + description: 'Add to epic', + warning: '', + icon: '', + params: ['\u003c\u0026epic | group\u0026epic | Epic URL\u003e'], + }, + { + name: 'remove_epic', + aliases: [], + description: 'Remove from epic', + warning: '', + icon: '', + params: [], + }, + { + name: 'promote', + aliases: [], + description: 'Promote issue to an epic', + warning: '', + icon: 'confidential', + params: [], + }, + { + name: 'iteration', + aliases: [], + description: 'Set iteration', + warning: '', + icon: '', + params: ['*iteration:"iteration name" | *iteration:\u003cID\u003e'], + }, + { + name: 'health_status', + aliases: [], + description: 'Set health status', + warning: '', + icon: '', + params: ['\u003con_track|needs_attention|at_risk\u003e'], + }, + { + name: 'reassign', + aliases: [], + description: 'Change assignees', + warning: '', + icon: '', + params: ['@user1 @user2'], + }, + { + name: 'weight', + aliases: [], + description: 'Set weight', + warning: '', + icon: '', + params: ['0, 1, 2, …'], + }, + { + name: 'blocks', + aliases: [], + description: 'Specifies that this issue blocks other issues', + warning: '', + icon: '', + params: ['\u003c#issue | group/project#issue | issue URL\u003e'], + }, + { + name: 'blocked_by', + aliases: [], + description: 'Mark this issue as blocked by other issues', + warning: '', + icon: '', + params: ['\u003c#issue | group/project#issue | issue URL\u003e'], + }, +]; diff --git a/spec/frontend/content_editor/services/data_source_factory_spec.js b/spec/frontend/content_editor/services/data_source_factory_spec.js new file mode 100644 index 00000000000..d540f11711d --- /dev/null +++ b/spec/frontend/content_editor/services/data_source_factory_spec.js @@ -0,0 +1,202 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import DataSourceFactory, { + defaultSorter, + customSorter, + createDataSource, +} from '~/content_editor/services/data_source_factory'; +import { + MOCK_MEMBERS, + MOCK_COMMANDS, + MOCK_EPICS, + MOCK_ISSUES, + MOCK_LABELS, + MOCK_MILESTONES, + MOCK_SNIPPETS, + MOCK_VULNERABILITIES, + MOCK_MERGE_REQUESTS, + MOCK_ASSIGNEES, + MOCK_REVIEWERS, +} from './autocomplete_mock_data'; + +jest.mock('~/emoji'); + +describe('defaultSorter', () => { + it('returns items as is if query is empty', () => { + const items = [{ name: 'abc' }, { name: 'bcd' }, { name: 'cde' }]; + const sorter = defaultSorter(['name']); + expect(sorter(items, '')).toEqual(items); + }); + + it('sorts items based on query match', () => { + const items = [{ name: 'abc' }, { name: 'bcd' }, { name: 'cde' }]; + const sorter = defaultSorter(['name']); + expect(sorter(items, 'b')).toEqual([{ name: 'bcd' }, { name: 'abc' }, { name: 'cde' }]); + }); + + it('sorts items based on query match in multiple fields', () => { + const items = [ + { name: 'wabc', description: 'xyz' }, + { name: 'bcd', description: 'wxy' }, + { name: 'cde', description: 'vwx' }, + ]; + const sorter = defaultSorter(['name', 'description']); + expect(sorter(items, 'w')).toEqual([ + { name: 'wabc', description: 'xyz' }, + { name: 'bcd', description: 'wxy' }, + { name: 'cde', description: 'vwx' }, + ]); + }); +}); + +describe('customSorter', () => { + it('sorts items based on custom sorter function', () => { + const items = [3, 1, 2]; + const sorter = customSorter((a, b) => a - b); + expect(sorter(items)).toEqual([1, 2, 3]); + }); +}); + +describe('createDataSource', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('fetches data from source and filters based on query', async () => { + const data = [ + { name: 'abc', description: 'xyz' }, + { name: 'bcd', description: 'wxy' }, + { name: 'cde', description: 'vwx' }, + ]; + mock.onGet('/source').reply(200, data); + + const dataSource = createDataSource({ + source: '/source', + searchFields: ['name', 'description'], + }); + + const results = await dataSource.search('b'); + expect(results).toEqual([ + { name: 'bcd', description: 'wxy' }, + { name: 'abc', description: 'xyz' }, + ]); + }); + + it('handles source fetch errors', async () => { + mock.onGet('/source').reply(500); + + const dataSource = createDataSource({ + source: '/source', + searchFields: ['name', 'description'], + sorter: (items) => items, + }); + + const results = await dataSource.search('b'); + expect(results).toEqual([]); + }); +}); + +describe('DataSourceFactory', () => { + let mock; + let autocompleteHelper; + let dateNowOld; + + beforeEach(() => { + mock = new MockAdapter(axios); + const dataSourceUrls = { + members: '/members', + issues: '/issues', + snippets: '/snippets', + labels: '/labels', + epics: '/epics', + milestones: '/milestones', + mergeRequests: '/mergeRequests', + vulnerabilities: '/vulnerabilities', + commands: '/commands', + }; + + mock.onGet('/members').reply(200, MOCK_MEMBERS); + mock.onGet('/issues').reply(200, MOCK_ISSUES); + mock.onGet('/snippets').reply(200, MOCK_SNIPPETS); + mock.onGet('/labels').reply(200, MOCK_LABELS); + mock.onGet('/epics').reply(200, MOCK_EPICS); + mock.onGet('/milestones').reply(200, MOCK_MILESTONES); + mock.onGet('/mergeRequests').reply(200, MOCK_MERGE_REQUESTS); + mock.onGet('/vulnerabilities').reply(200, MOCK_VULNERABILITIES); + mock.onGet('/commands').reply(200, MOCK_COMMANDS); + + const sidebarMediator = { + store: { + assignees: MOCK_ASSIGNEES, + reviewers: MOCK_REVIEWERS, + }, + }; + + autocompleteHelper = new DataSourceFactory({ + dataSourceUrls, + sidebarMediator, + }); + + dateNowOld = Date.now(); + + jest.spyOn(Date, 'now').mockImplementation(() => new Date('2023-11-14').getTime()); + }); + + afterEach(() => { + mock.restore(); + + jest.spyOn(Date, 'now').mockImplementation(() => dateNowOld); + }); + + it.each` + referenceType | query + ${'user'} | ${'r'} + ${'issue'} | ${'q'} + ${'snippet'} | ${'s'} + ${'label'} | ${'c'} + ${'epic'} | ${'n'} + ${'milestone'} | ${'16'} + ${'merge_request'} | ${'n'} + ${'vulnerability'} | ${'cross'} + ${'command'} | ${'re'} + `( + 'for reference type "$referenceType", searches for "$query" correctly', + async ({ referenceType, query }) => { + const dataSource = autocompleteHelper.getDataSource(referenceType); + const results = await dataSource.search(query); + + expect( + results.map(({ title, name, username }) => username || name || title), + ).toMatchSnapshot(); + }, + ); + + it.each` + referenceType | command + ${'label'} | ${'/label'} + ${'label'} | ${'/unlabel'} + ${'label'} | ${'/relabel'} + ${'user'} | ${'/assign'} + ${'user'} | ${'/reassign'} + ${'user'} | ${'/unassign'} + ${'user'} | ${'/assign_reviewer'} + ${'user'} | ${'/unassign_reviewer'} + ${'user'} | ${'/reassign_reviewer'} + `( + 'filters items based on command "$command" for reference type "$referenceType" and command', + async ({ referenceType, command }) => { + const dataSource = autocompleteHelper.getDataSource(referenceType, { command }); + const results = await dataSource.search(); + + expect( + results.map(({ username, name, title }) => username || name || title), + ).toMatchSnapshot(); + }, + ); +}); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 548c6030ed7..c329a12bcc4 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -152,19 +152,26 @@ describe('markdownSerializer', () => { expect(serialize(paragraph(italic('italics')))).toBe('_italics_'); }); - it('correctly serializes code blocks wrapped by italics and bold marks', () => { - const codeBlockContent = 'code block'; - - expect(serialize(paragraph(italic(code(codeBlockContent))))).toBe(`_\`${codeBlockContent}\`_`); - expect(serialize(paragraph(code(italic(codeBlockContent))))).toBe(`_\`${codeBlockContent}\`_`); - expect(serialize(paragraph(bold(code(codeBlockContent))))).toBe(`**\`${codeBlockContent}\`**`); - expect(serialize(paragraph(code(bold(codeBlockContent))))).toBe(`**\`${codeBlockContent}\`**`); - expect(serialize(paragraph(strike(code(codeBlockContent))))).toBe( - `~~\`${codeBlockContent}\`~~`, - ); - expect(serialize(paragraph(code(strike(codeBlockContent))))).toBe( - `~~\`${codeBlockContent}\`~~`, - ); + it.each` + input | output + ${'code'} | ${'`code`'} + ${'code `with` backticks'} | ${'``code `with` backticks``'} + ${'this is `inline-code`'} | ${'`` this is `inline-code` ``'} + ${'`inline-code` in markdown'} | ${'`` `inline-code` in markdown ``'} + ${'```js'} | ${'`` ```js ``'} + `('correctly serializes inline code ("$input")', ({ input, output }) => { + expect(serialize(paragraph(code(input)))).toBe(output); + }); + + it('correctly serializes inline code wrapped by italics and bold marks', () => { + const content = 'code'; + + expect(serialize(paragraph(italic(code(content))))).toBe(`_\`${content}\`_`); + expect(serialize(paragraph(code(italic(content))))).toBe(`_\`${content}\`_`); + expect(serialize(paragraph(bold(code(content))))).toBe(`**\`${content}\`**`); + expect(serialize(paragraph(code(bold(content))))).toBe(`**\`${content}\`**`); + expect(serialize(paragraph(strike(code(content))))).toBe(`~~\`${content}\`~~`); + expect(serialize(paragraph(code(strike(content))))).toBe(`~~\`${content}\`~~`); }); it('correctly serializes inline diff', () => { @@ -461,6 +468,52 @@ this is not really json:table but just trying out whether this case works or not ); }); + it('correctly serializes a markdown code block containing a nested code block', () => { + expect( + serialize( + codeBlock( + { language: 'markdown' }, + 'markdown code block **bold** _italic_ `code`\n\n```js\nvar a = 0;\n```\n\nend markdown code block', + ), + ), + ).toBe( + ` +\`\`\`\`markdown +markdown code block **bold** _italic_ \`code\` + +\`\`\`js +var a = 0; +\`\`\` + +end markdown code block +\`\`\`\` + `.trim(), + ); + }); + + it('correctly serializes a markdown code block containing a markdown code block containing another code block', () => { + expect( + serialize( + codeBlock( + { language: 'markdown' }, + '````md\na nested code block\n\n```js\nvar a = 0;\n```\n````', + ), + ), + ).toBe( + ` +\`\`\`\`\`markdown +\`\`\`\`md +a nested code block + +\`\`\`js +var a = 0; +\`\`\` +\`\`\`\` +\`\`\`\`\` + `.trim(), + ); + }); + it('correctly serializes emoji', () => { expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:'); }); @@ -607,6 +660,34 @@ this is not really json:table but just trying out whether this case works or not ); }); + it('correctly serializes bullet task list with different bullet styles', () => { + expect( + serialize( + taskList( + { bullet: '+' }, + taskItem({ checked: true }, paragraph('list item 1')), + taskItem(paragraph('list item 2')), + taskItem( + paragraph('list item 3'), + taskList( + { bullet: '-' }, + taskItem({ checked: true }, paragraph('sub-list item 1')), + taskItem(paragraph('sub-list item 2')), + ), + ), + ), + ), + ).toBe( + ` ++ [x] list item 1 ++ [ ] list item 2 ++ [ ] list item 3 + - [x] sub-list item 1 + - [ ] sub-list item 2 + `.trim(), + ); + }); + it('correctly serializes a numeric list', () => { expect( serialize( diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js index 2efc73ddef8..4428fa682e7 100644 --- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js +++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js @@ -1,6 +1,8 @@ import { Extension } from '@tiptap/core'; import BulletList from '~/content_editor/extensions/bullet_list'; import ListItem from '~/content_editor/extensions/list_item'; +import TaskList from '~/content_editor/extensions/task_list'; +import TaskItem from '~/content_editor/extensions/task_item'; import Paragraph from '~/content_editor/extensions/paragraph'; import markdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer'; import { getMarkdownSource, getFullSource } from '~/content_editor/services/markdown_sourcemap'; @@ -18,6 +20,20 @@ const BULLET_LIST_HTML = `<ul data-sourcepos="1:1-3:24" dir="auto"> </li> </ul>`; +const BULLET_TASK_LIST_MARKDOWN = `- [ ] list item 1 ++ [x] checked list item 2 + + [ ] embedded list item 1 + - [x] checked embedded list item 2`; +const BULLET_TASK_LIST_HTML = `<ul data-sourcepos="1:1-4:36" class="task-list" dir="auto"> + <li data-sourcepos="1:1-1:17" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox"> list item 1</li> + <li data-sourcepos="2:1-4:36" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" checked> checked list item 2 + <ul data-sourcepos="3:3-4:36" class="task-list"> + <li data-sourcepos="3:3-3:28" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox"> embedded list item 1</li> + <li data-sourcepos="4:3-4:36" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" checked> checked embedded list item 2</li> + </ul> + </li> +</ul>`; + const SourcemapExtension = Extension.create({ // lets add `source` attribute to every element using `getMarkdownSource` addGlobalAttributes() { @@ -38,19 +54,68 @@ const SourcemapExtension = Extension.create({ }); const tiptapEditor = createTestEditor({ - extensions: [BulletList, ListItem, SourcemapExtension], + extensions: [BulletList, ListItem, TaskList, TaskItem, SourcemapExtension], }); const { - builders: { doc, bulletList, listItem, paragraph }, + builders: { doc, bulletList, listItem, taskList, taskItem, paragraph }, } = createDocBuilder({ tiptapEditor, names: { bulletList: { nodeType: BulletList.name }, listItem: { nodeType: ListItem.name }, + taskList: { nodeType: TaskList.name }, + taskItem: { nodeType: TaskItem.name }, }, }); +const bulletListDoc = () => + doc( + bulletList( + { bullet: '+', source: '+ list item 1\n+ list item 2\n - embedded list item 3' }, + listItem({ source: '+ list item 1' }, paragraph('list item 1')), + listItem( + { source: '+ list item 2\n - embedded list item 3' }, + paragraph('list item 2'), + bulletList( + { bullet: '-', source: '- embedded list item 3' }, + listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')), + ), + ), + ), + ); + +const bulletTaskListDoc = () => + doc( + taskList( + { + bullet: '-', + source: + '- [ ] list item 1\n+ [x] checked list item 2\n + [ ] embedded list item 1\n - [x] checked embedded list item 2', + }, + taskItem({ source: '- [ ] list item 1' }, paragraph('list item 1')), + taskItem( + { + source: + '+ [x] checked list item 2\n + [ ] embedded list item 1\n - [x] checked embedded list item 2', + checked: true, + }, + paragraph('checked list item 2'), + taskList( + { + bullet: '+', + source: '+ [ ] embedded list item 1\n - [x] checked embedded list item 2', + }, + taskItem({ source: '+ [ ] embedded list item 1' }, paragraph('embedded list item 1')), + taskItem( + { source: '- [x] checked embedded list item 2', checked: true }, + paragraph('checked embedded list item 2'), + ), + ), + ), + ), + ); + describe('content_editor/services/markdown_sourcemap', () => { describe('getFullSource', () => { it.each` @@ -72,29 +137,21 @@ describe('content_editor/services/markdown_sourcemap', () => { }); }); - it('gets markdown source for a rendered HTML element', async () => { - const { document } = await markdownDeserializer({ - render: () => BULLET_LIST_HTML, - }).deserialize({ - schema: tiptapEditor.schema, - markdown: BULLET_LIST_MARKDOWN, - }); - - const expected = doc( - bulletList( - { bullet: '+', source: '+ list item 1\n+ list item 2' }, - listItem({ source: '+ list item 1' }, paragraph('list item 1')), - listItem( - { source: '+ list item 2' }, - paragraph('list item 2'), - bulletList( - { bullet: '-', source: '- embedded list item 3' }, - listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')), - ), - ), - ), - ); + it.each` + description | sourceMarkdown | sourceHTML | expectedDoc + ${'bullet list'} | ${BULLET_LIST_MARKDOWN} | ${BULLET_LIST_HTML} | ${bulletListDoc} + ${'bullet task list'} | ${BULLET_TASK_LIST_MARKDOWN} | ${BULLET_TASK_LIST_HTML} | ${bulletTaskListDoc} + `( + 'gets markdown source for a rendered $description', + async ({ sourceMarkdown, sourceHTML, expectedDoc }) => { + const { document } = await markdownDeserializer({ + render: () => sourceHTML, + }).deserialize({ + schema: tiptapEditor.schema, + markdown: sourceMarkdown, + }); - expect(document.toJSON()).toEqual(expected.toJSON()); - }); + expect(document.toJSON()).toEqual(expectedDoc().toJSON()); + }, + ); }); diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js index cbd4f555e97..255a7104eaf 100644 --- a/spec/frontend/content_editor/test_constants.js +++ b/spec/frontend/content_editor/test_constants.js @@ -44,3 +44,18 @@ export const RESOLVED_MERGE_REQUEST_HTML = export const RESOLVED_EPIC_HTML = '<p data-sourcepos="1:1-1:11" dir="auto"><a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;1" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">&1</a> <a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;1+" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-reference-format="+" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">Approvals in merge request list (&1)</a> <a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;1+s" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-reference-format="+s" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">Approvals in merge request list (&1)</a></p>'; + +export const RESOLVED_LABEL_HTML = + '<p data-sourcepos="1:1-1:29" dir="auto"><span class="gl-label gl-label-sm"><a href="/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix" data-reference-type="label" data-original="~Aquanix" data-link="false" data-link-reference="false" data-project="2" data-label="5" data-container="body" data-placement="top" title="" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #e65431">Aquanix</span></a></span> <span class="gl-label gl-label-sm"><a href="/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix" data-reference-type="label" data-original="~Aquanix" data-link="false" data-link-reference="false" data-project="2" data-label="5" data-container="body" data-placement="top" title="" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #e65431">Aquanix</span></a></span>+ <span class="gl-label gl-label-sm"><a href="/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix" data-reference-type="label" data-original="~Aquanix" data-link="false" data-link-reference="false" data-project="2" data-label="5" data-container="body" data-placement="top" title="" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #e65431">Aquanix</span></a></span>+s</p>'; + +export const RESOLVED_SNIPPET_HTML = + '<p data-sourcepos="1:1-1:14" dir="auto"><a href="/gitlab-org/gitlab-shell/-/snippets/25" data-reference-type="snippet" data-original="$25" data-link="false" data-link-reference="false" data-project="2" data-snippet="25" data-container="body" data-placement="top" title="test" class="gfm gfm-snippet has-tooltip">$25</a> <a href="/gitlab-org/gitlab-shell/-/snippets/25" data-reference-type="snippet" data-original="$25" data-link="false" data-link-reference="false" data-project="2" data-snippet="25" data-container="body" data-placement="top" title="test" class="gfm gfm-snippet has-tooltip">$25</a>+ <a href="/gitlab-org/gitlab-shell/-/snippets/25" data-reference-type="snippet" data-original="$25" data-link="false" data-link-reference="false" data-project="2" data-snippet="25" data-container="body" data-placement="top" title="test" class="gfm gfm-snippet has-tooltip">$25</a>+s</p>'; + +export const RESOLVED_MILESTONE_HTML = + '<p data-sourcepos="1:1-1:20" dir="auto"><a href="/gitlab-org/gitlab-shell/-/milestones/5" data-reference-type="milestone" data-original="%v4.0" data-link="false" data-link-reference="false" data-project="2" data-milestone="10" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a> <a href="/gitlab-org/gitlab-shell/-/milestones/5" data-reference-type="milestone" data-original="%v4.0" data-link="false" data-link-reference="false" data-project="2" data-milestone="10" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a>+ %v4.0+s</p>'; + +export const RESOLVED_USER_HTML = + '<p data-sourcepos="1:1-1:20" dir="auto"><a href="/root" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Administrator">@root</a> <a href="/root" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Administrator">@root</a>+ <a href="/root" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Administrator">@root</a>+s</p>'; + +export const RESOLVED_VULNERABILITY_HTML = + '<p data-sourcepos="1:1-1:56" dir="auto"><a href="/gitlab-org/gitlab-shell/-/security/vulnerabilities/1" data-reference-type="vulnerability" data-original="[vulnerability:1]" data-link="false" data-link-reference="false" data-project="2" data-vulnerability="1" data-container="body" data-placement="top" title="oh no!" class="gfm gfm-vulnerability has-tooltip">[vulnerability:1]</a> <a href="/gitlab-org/gitlab-shell/-/security/vulnerabilities/1" data-reference-type="vulnerability" data-original="[vulnerability:1]" data-link="false" data-link-reference="false" data-project="2" data-vulnerability="1" data-container="body" data-placement="top" title="oh no!" class="gfm gfm-vulnerability has-tooltip">[vulnerability:1]</a>+ <a href="/gitlab-org/gitlab-shell/-/security/vulnerabilities/1" data-reference-type="vulnerability" data-original="[vulnerability:1]" data-link="false" data-link-reference="false" data-project="2" data-vulnerability="1" data-container="body" data-placement="top" title="oh no!" class="gfm gfm-vulnerability has-tooltip">[vulnerability:1]</a>+s</p>'; |