diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-01 12:08:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-01 12:08:29 +0300 |
commit | 4def415fbf45e0693b17ea418d378d62ab03a146 (patch) | |
tree | 48fa3f684b33eefbd37e57b7bbe1a17926825e6a /spec/frontend/vue_shared/components/filtered_search_bar | |
parent | a6dce21d917a0a359b3521ec3cef02ab3e6199cf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/vue_shared/components/filtered_search_bar')
3 files changed, 711 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index e3e2ef5610d..86d1f21fd04 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -8,6 +8,8 @@ import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; +import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue'; +import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue'; export const mockAuthor1 = { id: 1, @@ -62,6 +64,128 @@ export const mockMilestones = [ mockEscapedMilestone, ]; +export const mockCrmContacts = [ + { + id: 'gid://gitlab/CustomerRelations::Contact/1', + firstName: 'John', + lastName: 'Smith', + email: 'john@smith.com', + }, + { + id: 'gid://gitlab/CustomerRelations::Contact/2', + firstName: 'Andy', + lastName: 'Green', + email: 'andy@green.net', + }, +]; + +export const mockCrmOrganizations = [ + { + id: 'gid://gitlab/CustomerRelations::Organization/1', + name: 'First Org Ltd.', + }, + { + id: 'gid://gitlab/CustomerRelations::Organization/2', + name: 'Organizer S.p.a.', + }, +]; + +export const mockProjectCrmContactsQueryResponse = { + data: { + project: { + __typename: 'Project', + id: 1, + group: { + __typename: 'Group', + id: 1, + contacts: { + __typename: 'CustomerRelationsContactConnection', + nodes: [ + { + __typename: 'CustomerRelationsContact', + ...mockCrmContacts[0], + }, + { + __typename: 'CustomerRelationsContact', + ...mockCrmContacts[1], + }, + ], + }, + }, + }, + }, +}; + +export const mockProjectCrmOrganizationsQueryResponse = { + data: { + project: { + __typename: 'Project', + id: 1, + group: { + __typename: 'Group', + id: 1, + organizations: { + __typename: 'CustomerRelationsOrganizationConnection', + nodes: [ + { + __typename: 'CustomerRelationsOrganization', + ...mockCrmOrganizations[0], + }, + { + __typename: 'CustomerRelationsOrganization', + ...mockCrmOrganizations[1], + }, + ], + }, + }, + }, + }, +}; + +export const mockGroupCrmContactsQueryResponse = { + data: { + group: { + __typename: 'Group', + id: 1, + contacts: { + __typename: 'CustomerRelationsContactConnection', + nodes: [ + { + __typename: 'CustomerRelationsContact', + ...mockCrmContacts[0], + }, + { + __typename: 'CustomerRelationsContact', + ...mockCrmContacts[1], + }, + ], + }, + }, + }, +}; + +export const mockGroupCrmOrganizationsQueryResponse = { + data: { + group: { + __typename: 'Group', + id: 1, + organizations: { + __typename: 'CustomerRelationsOrganizationConnection', + nodes: [ + { + __typename: 'CustomerRelationsOrganization', + ...mockCrmOrganizations[0], + }, + { + __typename: 'CustomerRelationsOrganization', + ...mockCrmOrganizations[1], + }, + ], + }, + }, + }, +}; + export const mockEmoji1 = { name: 'thumbsup', }; @@ -134,6 +258,28 @@ export const mockReactionEmojiToken = { fetchEmojis: () => Promise.resolve(mockEmojis), }; +export const mockCrmContactToken = { + type: 'crm_contact', + title: 'Contact', + icon: 'user', + token: CrmContactToken, + isProject: false, + fullPath: 'group', + operators: OPERATOR_IS_ONLY, + unique: true, +}; + +export const mockCrmOrganizationToken = { + type: 'crm_contact', + title: 'Organization', + icon: 'user', + token: CrmOrganizationToken, + isProject: false, + fullPath: 'group', + operators: OPERATOR_IS_ONLY, + unique: true, +}; + export const mockMembershipToken = { type: 'with_inherited_permissions', icon: 'group', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js new file mode 100644 index 00000000000..157e021fc60 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js @@ -0,0 +1,283 @@ +import { + GlFilteredSearchSuggestion, + GlFilteredSearchTokenSegment, + GlDropdownDivider, +} from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue'; +import searchCrmContactsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql'; + +import { + mockCrmContacts, + mockCrmContactToken, + mockGroupCrmContactsQueryResponse, + mockProjectCrmContactsQueryResponse, +} from '../mock_data'; + +jest.mock('~/flash'); + +const defaultStubs = { + Portal: true, + BaseToken, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, +}; + +describe('CrmContactToken', () => { + Vue.use(VueApollo); + + let wrapper; + let fakeApollo; + + const getBaseToken = () => wrapper.findComponent(BaseToken); + + const searchGroupCrmContactsQueryHandler = jest + .fn() + .mockResolvedValue(mockGroupCrmContactsQueryResponse); + const searchProjectCrmContactsQueryHandler = jest + .fn() + .mockResolvedValue(mockProjectCrmContactsQueryResponse); + + const mountComponent = ({ + config = mockCrmContactToken, + value = { data: '' }, + active = false, + stubs = defaultStubs, + listeners = {}, + queryHandler = searchGroupCrmContactsQueryHandler, + } = {}) => { + fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]); + + wrapper = mount(CrmContactToken, { + propsData: { + config, + value, + active, + cursorPosition: 'start', + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: () => 'custom-class', + }, + stubs, + listeners, + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + describe('methods', () => { + describe('fetchContacts', () => { + describe('for groups', () => { + beforeEach(() => { + mountComponent(); + }); + + it('calls the apollo query providing the searchString when search term is a string', async () => { + getBaseToken().vm.$emit('fetch-suggestions', 'foo'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'group', + isProject: false, + searchString: 'foo', + searchIds: null, + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts); + }); + + it('calls the apollo query providing the searchId when search term is a number', async () => { + getBaseToken().vm.$emit('fetch-suggestions', '5'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'group', + isProject: false, + searchString: null, + searchIds: ['gid://gitlab/CustomerRelations::Contact/5'], + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts); + }); + }); + + describe('for projects', () => { + beforeEach(() => { + mountComponent({ + config: { + fullPath: 'project', + isProject: true, + }, + queryHandler: searchProjectCrmContactsQueryHandler, + }); + }); + + it('calls the apollo query providing the searchString when search term is a string', async () => { + getBaseToken().vm.$emit('fetch-suggestions', 'foo'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'project', + isProject: true, + searchString: 'foo', + searchIds: null, + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts); + }); + + it('calls the apollo query providing the searchId when search term is a number', async () => { + getBaseToken().vm.$emit('fetch-suggestions', '5'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'project', + isProject: true, + searchString: null, + searchIds: ['gid://gitlab/CustomerRelations::Contact/5'], + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts); + }); + }); + + it('calls `createFlash` with flash error message when request fails', async () => { + mountComponent(); + + jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({}); + + getBaseToken().vm.$emit('fetch-suggestions'); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was a problem fetching CRM contacts.', + }); + }); + + it('sets `loading` to false when request completes', async () => { + mountComponent(); + + jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({}); + + getBaseToken().vm.$emit('fetch-suggestions'); + + await waitForPromises(); + + expect(getBaseToken().props('suggestionsLoading')).toBe(false); + }); + }); + }); + + describe('template', () => { + const defaultContacts = DEFAULT_NONE_ANY; + + it('renders base-token component', () => { + mountComponent({ + config: { ...mockCrmContactToken, initialContacts: mockCrmContacts }, + value: { data: '1' }, + }); + + const baseTokenEl = wrapper.find(BaseToken); + + expect(baseTokenEl.exists()).toBe(true); + expect(baseTokenEl.props()).toMatchObject({ + suggestions: mockCrmContacts, + getActiveTokenValue: wrapper.vm.getActiveContact, + }); + }); + + it.each(mockCrmContacts)('renders token item when value is selected', (contact) => { + mountComponent({ + config: { ...mockCrmContactToken, initialContacts: mockCrmContacts }, + value: { data: `${getIdFromGraphQLId(contact.id)}` }, + }); + + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // Contact, =, Contact name + expect(tokenSegments.at(2).text()).toBe(`${contact.firstName} ${contact.lastName}`); // Contact name + }); + + it('renders provided defaultContacts as suggestions', async () => { + mountComponent({ + active: true, + config: { ...mockCrmContactToken, defaultContacts }, + stubs: { Portal: true }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await nextTick(); + + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(defaultContacts.length); + defaultContacts.forEach((contact, index) => { + expect(suggestions.at(index).text()).toBe(contact.text); + }); + }); + + it('does not render divider when no defaultContacts', async () => { + mountComponent({ + active: true, + config: { ...mockCrmContactToken, defaultContacts: [] }, + stubs: { Portal: true }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await nextTick(); + + expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + }); + + it('renders `DEFAULT_NONE_ANY` as default suggestions', () => { + mountComponent({ + active: true, + config: { ...mockCrmContactToken }, + stubs: { Portal: true }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length); + DEFAULT_NONE_ANY.forEach((contact, index) => { + expect(suggestions.at(index).text()).toBe(contact.text); + }); + }); + + it('emits listeners in the base-token', () => { + const mockInput = jest.fn(); + mountComponent({ + listeners: { + input: mockInput, + }, + }); + wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]); + + expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js new file mode 100644 index 00000000000..977f8bbef61 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js @@ -0,0 +1,282 @@ +import { + GlFilteredSearchSuggestion, + GlFilteredSearchTokenSegment, + GlDropdownDivider, +} from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue'; +import searchCrmOrganizationsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql'; + +import { + mockCrmOrganizations, + mockCrmOrganizationToken, + mockGroupCrmOrganizationsQueryResponse, + mockProjectCrmOrganizationsQueryResponse, +} from '../mock_data'; + +jest.mock('~/flash'); + +const defaultStubs = { + Portal: true, + BaseToken, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, +}; + +describe('CrmOrganizationToken', () => { + Vue.use(VueApollo); + + let wrapper; + let fakeApollo; + + const getBaseToken = () => wrapper.findComponent(BaseToken); + + const searchGroupCrmOrganizationsQueryHandler = jest + .fn() + .mockResolvedValue(mockGroupCrmOrganizationsQueryResponse); + const searchProjectCrmOrganizationsQueryHandler = jest + .fn() + .mockResolvedValue(mockProjectCrmOrganizationsQueryResponse); + + const mountComponent = ({ + config = mockCrmOrganizationToken, + value = { data: '' }, + active = false, + stubs = defaultStubs, + listeners = {}, + queryHandler = searchGroupCrmOrganizationsQueryHandler, + } = {}) => { + fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]); + wrapper = mount(CrmOrganizationToken, { + propsData: { + config, + value, + active, + cursorPosition: 'start', + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + suggestionsListClass: () => 'custom-class', + }, + stubs, + listeners, + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + describe('methods', () => { + describe('fetchOrganizations', () => { + describe('for groups', () => { + beforeEach(() => { + mountComponent(); + }); + + it('calls the apollo query providing the searchString when search term is a string', async () => { + getBaseToken().vm.$emit('fetch-suggestions', 'foo'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'group', + isProject: false, + searchString: 'foo', + searchIds: null, + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations); + }); + + it('calls the apollo query providing the searchId when search term is a number', async () => { + getBaseToken().vm.$emit('fetch-suggestions', '5'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'group', + isProject: false, + searchString: null, + searchIds: ['gid://gitlab/CustomerRelations::Organization/5'], + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations); + }); + }); + + describe('for projects', () => { + beforeEach(() => { + mountComponent({ + config: { + fullPath: 'project', + isProject: true, + }, + queryHandler: searchProjectCrmOrganizationsQueryHandler, + }); + }); + + it('calls the apollo query providing the searchString when search term is a string', async () => { + getBaseToken().vm.$emit('fetch-suggestions', 'foo'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'project', + isProject: true, + searchString: 'foo', + searchIds: null, + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations); + }); + + it('calls the apollo query providing the searchId when search term is a number', async () => { + getBaseToken().vm.$emit('fetch-suggestions', '5'); + await waitForPromises(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({ + fullPath: 'project', + isProject: true, + searchString: null, + searchIds: ['gid://gitlab/CustomerRelations::Organization/5'], + }); + expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations); + }); + }); + + it('calls `createFlash` with flash error message when request fails', async () => { + mountComponent(); + + jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({}); + + getBaseToken().vm.$emit('fetch-suggestions'); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was a problem fetching CRM organizations.', + }); + }); + + it('sets `loading` to false when request completes', async () => { + mountComponent(); + + jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({}); + + getBaseToken().vm.$emit('fetch-suggestions'); + + await waitForPromises(); + + expect(getBaseToken().props('suggestionsLoading')).toBe(false); + }); + }); + }); + + describe('template', () => { + const defaultOrganizations = DEFAULT_NONE_ANY; + + it('renders base-token component', () => { + mountComponent({ + config: { ...mockCrmOrganizationToken, initialOrganizations: mockCrmOrganizations }, + value: { data: '1' }, + }); + + const baseTokenEl = wrapper.find(BaseToken); + + expect(baseTokenEl.exists()).toBe(true); + expect(baseTokenEl.props()).toMatchObject({ + suggestions: mockCrmOrganizations, + getActiveTokenValue: wrapper.vm.getActiveOrganization, + }); + }); + + it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => { + mountComponent({ + config: { ...mockCrmOrganizationToken, initialOrganizations: mockCrmOrganizations }, + value: { data: `${getIdFromGraphQLId(organization.id)}` }, + }); + + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // Organization, =, Organization name + expect(tokenSegments.at(2).text()).toBe(organization.name); // Organization name + }); + + it('renders provided defaultOrganizations as suggestions', async () => { + mountComponent({ + active: true, + config: { ...mockCrmOrganizationToken, defaultOrganizations }, + stubs: { Portal: true }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await nextTick(); + + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(defaultOrganizations.length); + defaultOrganizations.forEach((organization, index) => { + expect(suggestions.at(index).text()).toBe(organization.text); + }); + }); + + it('does not render divider when no defaultOrganizations', async () => { + mountComponent({ + active: true, + config: { ...mockCrmOrganizationToken, defaultOrganizations: [] }, + stubs: { Portal: true }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await nextTick(); + + expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); + expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); + }); + + it('renders `DEFAULT_NONE_ANY` as default suggestions', () => { + mountComponent({ + active: true, + config: { ...mockCrmOrganizationToken }, + stubs: { Portal: true }, + }); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + + const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); + + expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length); + DEFAULT_NONE_ANY.forEach((organization, index) => { + expect(suggestions.at(index).text()).toBe(organization.text); + }); + }); + + it('emits listeners in the base-token', () => { + const mockInput = jest.fn(); + mountComponent({ + listeners: { + input: mockInput, + }, + }); + wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]); + + expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]); + }); + }); +}); |