From 1bdc6c89c32a7380a81598629b9ad05ba9a2a94f Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 7 Dec 2023 18:07:33 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../components/content_editor_spec.js | 14 +- .../components/suggestions_dropdown_spec.js | 188 +++- .../__snapshots__/data_source_factory_spec.js.snap | 256 ++++++ .../services/autocomplete_mock_data.js | 967 +++++++++++++++++++++ .../services/data_source_factory_spec.js | 202 +++++ spec/frontend/gfm_auto_complete_spec.js | 8 +- .../service/archived_projects_service_spec.js | 2 +- 7 files changed, 1599 insertions(+), 38 deletions(-) create mode 100644 spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap create mode 100644 spec/frontend/content_editor/services/autocomplete_mock_data.js create mode 100644 spec/frontend/content_editor/services/data_source_factory_spec.js (limited to 'spec/frontend') 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'} | ${'root'} + ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'Administrator'} + ${'reference'} | ${'issue'} | ${exampleIssue} | ${'test'} | ${'Test Issue'} + ${'reference'} | ${'issue'} | ${exampleIssue} | ${'12'} | ${'123'} + ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'test'} | ${'Test MR'} + ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'22'} | ${'224'} + ${'reference'} | ${'epic'} | ${exampleEpic} | ${'rem'} | ${'❓ Remote Development | Solution validation'} + ${'reference'} | ${'epic'} | ${exampleEpic} | ${'88'} | ${'gitlab-org&8884'} + ${'reference'} | ${'milestone'} | ${exampleMilestone1} | ${'1'} | ${'13'} + ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'due'} + ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'Set due date'} + ${'reference'} | ${'label'} | ${exampleLabel1} | ${'c'} | ${'Create'} + ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'network'} | ${'System procs network activity'} + ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'85'} | ${'60850147'} + ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'project'} | ${'Project creation QueryRecorder logs'} + ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'242'} | ${'2420859'} + ${'emoji'} | ${'emoji'} | ${exampleEmoji} | ${'sm'} | ${'smiley'} + `( + '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(` + + 😄 + + `); + expect(wrapper.findAllComponents('gl-emoji-stub').at(1).html()).toMatchInlineSnapshot(` + + 😸 + + `); + expect(wrapper.findAllComponents('gl-emoji-stub').at(2).html()).toMatchInlineSnapshot(` + + 😃 + + `); + expect(wrapper.findAllComponents('gl-emoji-stub').at(3).html()).toMatchInlineSnapshot(` + + `); }); }); }); 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/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/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 6cea75036bc..2d7841771a1 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -547,7 +547,7 @@ describe('GfmAutoComplete', () => { expect(membersBeforeSave([{ ...mockGroup, avatar_url: null }])).toEqual([ { username: 'my-group', - avatarTag: '
M
', + avatarTag: '
M
', title: 'My Group (2)', search: 'MyGroup my-group', icon: '', @@ -560,7 +560,7 @@ describe('GfmAutoComplete', () => { { username: 'my-group', avatarTag: - 'my-group', + 'my-group', title: 'My Group (2)', search: 'MyGroup my-group', icon: '', @@ -573,7 +573,7 @@ describe('GfmAutoComplete', () => { { username: 'my-group', avatarTag: - 'my-group', + 'my-group', title: 'My Group', search: 'MyGroup my-group', icon: @@ -591,7 +591,7 @@ describe('GfmAutoComplete', () => { { username: 'my-user', avatarTag: - 'my-user', + 'my-user', title: 'My User', search: 'MyUser my-user', icon: '', diff --git a/spec/frontend/groups/service/archived_projects_service_spec.js b/spec/frontend/groups/service/archived_projects_service_spec.js index 6bc46e4799c..988fb5553ba 100644 --- a/spec/frontend/groups/service/archived_projects_service_spec.js +++ b/spec/frontend/groups/service/archived_projects_service_spec.js @@ -30,7 +30,7 @@ describe('ArchivedProjectsService', () => { markdown_description: project.description_html, visibility: project.visibility, avatar_url: project.avatar_url, - relative_path: `/${project.path_with_namespace}`, + relative_path: `${gon.relative_url_root}/${project.path_with_namespace}`, edit_path: null, leave_path: null, can_edit: false, -- cgit v1.2.3