diff options
Diffstat (limited to 'spec/frontend/work_items/components')
27 files changed, 974 insertions, 215 deletions
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js index 826fc2b2230..b2b372d9d0d 100644 --- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js @@ -10,9 +10,11 @@ import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comme import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue'; import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { createWorkItemNoteResponse, + groupWorkItemByIidResponseFactory, workItemByIidResponseFactory, workItemQueryResponse, } from '../../mock_data'; @@ -29,6 +31,7 @@ describe('Work item add note', () => { const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse); let workItemResponseHandler; + let groupWorkItemResponseHandler; const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm); const findTextarea = () => wrapper.findByTestId('note-reply-textarea'); @@ -40,29 +43,32 @@ describe('Work item add note', () => { canCreateNote = true, workItemIid = '1', workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }), + groupWorkItemResponse = groupWorkItemByIidResponseFactory({ canUpdate, canCreateNote }), signedIn = true, isEditing = true, + isGroup = false, workItemType = 'Task', isInternalThread = false, } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + groupWorkItemResponseHandler = jest.fn().mockResolvedValue(groupWorkItemResponse); if (signedIn) { window.gon.current_user_id = '1'; window.gon.current_user_avatar_url = 'avatar.png'; } - const apolloProvider = createMockApollo([ - [workItemByIidQuery, workItemResponseHandler], - [createNoteMutation, mutationHandler], - ]); - const { id } = workItemQueryResponse.data.workItem; wrapper = shallowMountExtended(WorkItemAddNote, { - apolloProvider, + apolloProvider: createMockApollo([ + [workItemByIidQuery, workItemResponseHandler], + [groupWorkItemByIidQuery, groupWorkItemResponseHandler], + [createNoteMutation, mutationHandler], + ]), provide: { - fullPath: 'test-project-path', + isGroup, }, propsData: { + fullPath: 'test-project-path', workItemId: id, workItemIid, workItemType, @@ -272,16 +278,44 @@ describe('Work item add note', () => { }); }); - it('calls the work item query', async () => { - await createComponent(); + describe('when project context', () => { + it('calls the project work item query', async () => { + await createComponent(); + + expect(workItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', async () => { + await createComponent(); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('skips calling the project work item query when missing workItemIid', async () => { + await createComponent({ workItemIid: '', isEditing: false }); - expect(workItemResponseHandler).toHaveBeenCalled(); + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); }); - it('skips calling the work item query when missing workItemIid', async () => { - await createComponent({ workItemIid: '', isEditing: false }); + describe('when group context', () => { + it('skips calling the project work item query', async () => { + await createComponent({ isGroup: true }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + await createComponent({ isGroup: true }); - expect(workItemResponseHandler).not.toHaveBeenCalled(); + expect(groupWorkItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query when missing workItemIid', async () => { + await createComponent({ isGroup: true, workItemIid: '', isEditing: false }); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); }); it('wrapper adds `internal-note` class when internal thread', async () => { diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js index dd88f34ae4f..ee2b434bd75 100644 --- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js @@ -48,6 +48,7 @@ describe('Work item comment form component', () => { } = {}) => { wrapper = shallowMount(WorkItemCommentForm, { propsData: { + fullPath: 'test-project-path', workItemState, workItemId, workItemType, @@ -59,9 +60,6 @@ describe('Work item comment form component', () => { autocompleteDataSources: {}, isNewDiscussion, }, - provide: { - fullPath: 'test-project-path', - }, directives: { GlTooltip: createMockDirective('gl-tooltip'), }, diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js index 9d22a64f2cb..fa53ba54faa 100644 --- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js @@ -31,10 +31,8 @@ describe('Work Item Discussion', () => { workItemType = 'Task', } = {}) => { wrapper = shallowMount(WorkItemDiscussion, { - provide: { - fullPath: 'gitlab-org', - }, propsData: { + fullPath: 'gitlab-org', discussion, workItemId, workItemIid: '1', diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js index e4180b2d178..6a24987b737 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js @@ -48,6 +48,7 @@ describe('Work Item Note Actions', () => { } = {}) => { wrapper = shallowMountExtended(WorkItemNoteActions, { propsData: { + fullPath: 'gitlab-org', showReply, showEdit, workItemIid: '1', @@ -63,7 +64,6 @@ describe('Work Item Note Actions', () => { projectName, }, provide: { - fullPath: 'gitlab-org', glFeatures: { workItemsMvc2: true, }, diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js index d425f1e50dc..ce915635946 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js @@ -61,10 +61,8 @@ describe('Work Item Note Awards List', () => { }); wrapper = shallowMount(WorkItemNoteAwardsList, { - provide: { - fullPath, - }, propsData: { + fullPath, workItemIid, note, isModal: false, diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index 9049a69656a..2b4c9604382 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -15,8 +15,10 @@ import NoteActions from '~/work_items/components/notes/work_item_note_actions.vu import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue'; import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { + groupWorkItemByIidResponseFactory, mockAssignees, mockWorkItemCommentNote, updateWorkItemMutationResponse, @@ -68,6 +70,9 @@ describe('Work Item Note', () => { }); const workItemResponseHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory()); + const groupWorkItemResponseHandler = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory()); const workItemByAuthoredByDifferentUser = jest .fn() .mockResolvedValue(mockWorkItemByDifferentUser); @@ -90,6 +95,7 @@ describe('Work Item Note', () => { const createComponent = ({ note = mockWorkItemCommentNote, isFirstNote = false, + isGroup = false, updateNoteMutationHandler = successHandler, workItemId = mockWorkItemId, updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler, @@ -98,9 +104,10 @@ describe('Work Item Note', () => { } = {}) => { wrapper = shallowMount(WorkItemNote, { provide: { - fullPath: 'test-project-path', + isGroup, }, propsData: { + fullPath: 'test-project-path', workItemId, workItemIid: '1', note, @@ -112,6 +119,7 @@ describe('Work Item Note', () => { }, apolloProvider: mockApollo([ [workItemByIidQuery, workItemByIidResponseHandler], + [groupWorkItemByIidQuery, groupWorkItemResponseHandler], [updateWorkItemNoteMutation, updateNoteMutationHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], ]), @@ -442,4 +450,32 @@ describe('Work Item Note', () => { expect(findAwardsList().props('workItemIid')).toBe('1'); }); }); + + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(workItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupWorkItemResponseHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js index b86f9ff34ae..2e1a7983dec 100644 --- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js +++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js @@ -1,4 +1,4 @@ -import { GlLabel, GlIcon } from '@gitlab/ui'; +import { GlLabel, GlIcon, GlLink } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -33,7 +33,7 @@ describe('WorkItemLinkChildContents', () => { const findStatusIconComponent = () => wrapper.findByTestId('item-status-icon').findComponent(GlIcon); const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon'); - const findTitleEl = () => wrapper.findByTestId('item-title'); + const findTitleEl = () => wrapper.findComponent(GlLink); const findStatusTooltipComponent = () => wrapper.findComponent(RichTimestampTooltip); const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata); const findAllLabels = () => wrapper.findAllComponents(GlLabel); @@ -46,7 +46,6 @@ describe('WorkItemLinkChildContents', () => { propsData: { canUpdate, childItem, - childPath: '/gitlab-org/gitlab-test/-/work_items/4', }, }); }; diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js index 0098a2e0864..15c33bf5b1e 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -22,13 +22,12 @@ import { import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql'; -import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { convertWorkItemMutationResponse, projectWorkItemTypesQueryResponse, convertWorkItemMutationErrorResponse, - workItemByIidResponseFactory, + updateWorkItemNotificationsMutationResponse, } from '../mock_data'; jest.mock('~/lib/utils/common_utils'); @@ -38,10 +37,7 @@ describe('WorkItemActions component', () => { Vue.use(VueApollo); let wrapper; - let mockApollo; const mockWorkItemReference = 'gitlab-org/gitlab-test#1'; - const mockWorkItemIid = '1'; - const mockFullPath = 'gitlab-org/gitlab-test'; const mockWorkItemCreateNoteEmail = 'gitlab-incoming+gitlab-org-gitlab-test-2-ddpzuq0zd2wefzofcpcdr3dg7-issue-1@gmail.com'; @@ -75,14 +71,22 @@ describe('WorkItemActions component', () => { hide: jest.fn(), }; + const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse); const convertWorkItemMutationSuccessHandler = jest .fn() .mockResolvedValue(convertWorkItemMutationResponse); - const convertWorkItemMutationErrorHandler = jest .fn() .mockResolvedValue(convertWorkItemMutationErrorResponse); - const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse); + const toggleNotificationsOffHandler = jest + .fn() + .mockResolvedValue(updateWorkItemNotificationsMutationResponse(false)); + const toggleNotificationsOnHandler = jest + .fn() + .mockResolvedValue(updateWorkItemNotificationsMutationResponse(true)); + const toggleNotificationsFailureHandler = jest + .fn() + .mockRejectedValue(new Error('Failed to subscribe')); const createComponent = ({ canUpdate = true, @@ -90,35 +94,21 @@ describe('WorkItemActions component', () => { isConfidential = false, subscribed = false, isParentConfidential = false, - notificationsMock = [updateWorkItemNotificationsMutation, jest.fn()], convertWorkItemMutationHandler = convertWorkItemMutationSuccessHandler, + notificationsMutationHandler, workItemType = 'Task', workItemReference = mockWorkItemReference, workItemCreateNoteEmail = mockWorkItemCreateNoteEmail, - writeQueryCache = false, } = {}) => { - const handlers = [notificationsMock]; - mockApollo = createMockApollo([ - ...handlers, - [convertWorkItemMutation, convertWorkItemMutationHandler], - [projectWorkItemTypesQuery, typesQuerySuccessHandler], - ]); - - // Write the query cache only when required e.g., notification widget mutation is called - if (writeQueryCache) { - const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true }); - - mockApollo.clients.defaultClient.cache.writeQuery({ - query: workItemByIidQuery, - variables: { fullPath: mockFullPath, iid: mockWorkItemIid }, - data: workItemQueryResponse.data, - }); - } - wrapper = shallowMountExtended(WorkItemActions, { isLoggedIn: isLoggedIn(), - apolloProvider: mockApollo, + apolloProvider: createMockApollo([ + [projectWorkItemTypesQuery, typesQuerySuccessHandler], + [convertWorkItemMutation, convertWorkItemMutationHandler], + [updateWorkItemNotificationsMutation, notificationsMutationHandler], + ]), propsData: { + fullPath: 'gitlab-org/gitlab-test', workItemId: 'gid://gitlab/WorkItem/1', canUpdate, canDelete, @@ -128,10 +118,9 @@ describe('WorkItemActions component', () => { workItemType, workItemReference, workItemCreateNoteEmail, - workItemIid: '1', }, provide: { - fullPath: mockFullPath, + isGroup: false, glFeatures: { workItemsMvc2: true }, }, mocks: { @@ -159,7 +148,6 @@ describe('WorkItemActions component', () => { it('renders modal', () => { createComponent(); - expect(findModal().exists()).toBe(true); expect(findModal().props('visible')).toBe(false); }); @@ -247,59 +235,15 @@ describe('WorkItemActions component', () => { }); it('does not render when canDelete is false', () => { - createComponent({ - canDelete: false, - }); + createComponent({ canDelete: false }); expect(findDeleteButton().exists()).toBe(false); }); }); describe('notifications action', () => { - const errorMessage = 'Failed to subscribe'; - const notificationToggledOffMessage = 'Notifications turned off.'; - const notificationToggledOnMessage = 'Notifications turned on.'; - - const toggleNotificationsOffHandler = jest.fn().mockResolvedValue({ - data: { - updateWorkItemNotificationsSubscription: { - issue: { - id: 'gid://gitlab/WorkItem/1', - subscribed: false, - }, - errors: [], - }, - }, - }); - - const toggleNotificationsOnHandler = jest.fn().mockResolvedValue({ - data: { - updateWorkItemNotificationsSubscription: { - issue: { - id: 'gid://gitlab/WorkItem/1', - subscribed: true, - }, - errors: [], - }, - }, - }); - - const toggleNotificationsFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage)); - - const notificationsOffMock = [ - updateWorkItemNotificationsMutation, - toggleNotificationsOffHandler, - ]; - - const notificationsOnMock = [updateWorkItemNotificationsMutation, toggleNotificationsOnHandler]; - - const notificationsFailureMock = [ - updateWorkItemNotificationsMutation, - toggleNotificationsFailureHandler, - ]; - beforeEach(() => { - createComponent({ writeQueryCache: true }); + createComponent(); isLoggedIn.mockReturnValue(true); }); @@ -308,25 +252,26 @@ describe('WorkItemActions component', () => { }); it.each` - scenario | subscribedToNotifications | notificationsMock | subscribedState | toastMessage - ${'turned off'} | ${false} | ${notificationsOffMock} | ${false} | ${notificationToggledOffMessage} - ${'turned on'} | ${true} | ${notificationsOnMock} | ${true} | ${notificationToggledOnMessage} + scenario | subscribedToNotifications | notificationsMutationHandler | subscribed | toastMessage + ${'turned off'} | ${false} | ${toggleNotificationsOffHandler} | ${false} | ${'Notifications turned off.'} + ${'turned on'} | ${true} | ${toggleNotificationsOnHandler} | ${true} | ${'Notifications turned on.'} `( 'calls mutation and displays toast when notification toggle is $scenario', - async ({ subscribedToNotifications, notificationsMock, subscribedState, toastMessage }) => { - createComponent({ notificationsMock, writeQueryCache: true }); - - await waitForPromises(); + async ({ + subscribedToNotifications, + notificationsMutationHandler, + subscribed, + toastMessage, + }) => { + createComponent({ notificationsMutationHandler }); findNotificationsToggle().vm.$emit('change', subscribedToNotifications); - await waitForPromises(); - expect(notificationsMock[1]).toHaveBeenCalledWith({ + expect(notificationsMutationHandler).toHaveBeenCalledWith({ input: { - projectPath: mockFullPath, - iid: mockWorkItemIid, - subscribedState, + id: 'gid://gitlab/WorkItem/1', + subscribed, }, }); expect(toast).toHaveBeenCalledWith(toastMessage); @@ -334,15 +279,12 @@ describe('WorkItemActions component', () => { ); it('emits error when the update notification mutation fails', async () => { - createComponent({ notificationsMock: notificationsFailureMock, writeQueryCache: true }); - - await waitForPromises(); + createComponent({ notificationsMutationHandler: toggleNotificationsFailureHandler }); findNotificationsToggle().vm.$emit('change', false); - await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[errorMessage]]); + expect(wrapper.emitted('error')).toEqual([['Failed to subscribe']]); }); }); @@ -359,13 +301,11 @@ describe('WorkItemActions component', () => { it('promote key result to objective', async () => { createComponent({ workItemType: 'Key Result' }); - - // wait for work item types await waitForPromises(); expect(findPromoteButton().exists()).toBe(true); - findPromoteButton().vm.$emit('action'); + findPromoteButton().vm.$emit('action'); await waitForPromises(); expect(convertWorkItemMutationSuccessHandler).toHaveBeenCalled(); @@ -378,13 +318,11 @@ describe('WorkItemActions component', () => { workItemType: 'Key Result', convertWorkItemMutationHandler: convertWorkItemMutationErrorHandler, }); - - // wait for work item types await waitForPromises(); expect(findPromoteButton().exists()).toBe(true); - findPromoteButton().vm.$emit('action'); + findPromoteButton().vm.$emit('action'); await waitForPromises(); expect(convertWorkItemMutationErrorHandler).toHaveBeenCalled(); @@ -399,6 +337,7 @@ describe('WorkItemActions component', () => { createComponent(); expect(findCopyReferenceButton().exists()).toBe(true); + findCopyReferenceButton().vm.$emit('action'); expect(toast).toHaveBeenCalledWith('Reference copied'); @@ -421,6 +360,7 @@ describe('WorkItemActions component', () => { createComponent(); expect(findCopyCreateNoteEmailButton().exists()).toBe(true); + findCopyCreateNoteEmailButton().vm.$emit('action'); expect(toast).toHaveBeenCalledWith('Email address copied'); diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js index 50a8847032e..196e19791df 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -6,7 +6,8 @@ import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mockTracking } from 'helpers/tracking_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; +import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql'; +import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; @@ -53,6 +54,9 @@ describe('WorkItemAssignees component', () => { const successSearchQueryHandler = jest .fn() .mockResolvedValue(projectMembersResponseWithCurrentUser); + const successGroupSearchQueryHandler = jest + .fn() + .mockResolvedValue(projectMembersResponseWithCurrentUser); const successSearchQueryHandlerWithMoreAssignees = jest .fn() .mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage); @@ -75,19 +79,22 @@ describe('WorkItemAssignees component', () => { allowsMultipleAssignees = true, canInviteMembers = false, canUpdate = true, + isGroup = false, } = {}) => { const apolloProvider = createMockApollo([ - [userSearchQuery, searchQueryHandler], + [usersSearchQuery, searchQueryHandler], + [groupUsersSearchQuery, successGroupSearchQueryHandler], [currentUserQuery, currentUserQueryHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], ]); wrapper = mountExtended(WorkItemAssignees, { provide: { - fullPath: 'test-project-path', + isGroup, }, propsData: { assignees, + fullPath: 'test-project-path', workItemId, allowsMultipleAssignees, workItemType: TASK_TYPE_NAME, @@ -540,4 +547,36 @@ describe('WorkItemAssignees component', () => { expect(findTokenSelector().props('dropdownItems')).toHaveLength(2); }); + + describe('when project context', () => { + beforeEach(() => { + createComponent(); + findTokenSelector().vm.$emit('focus'); + findTokenSelector().vm.$emit('text-input', 'jane'); + }); + + it('calls the project users search query', () => { + expect(successSearchQueryHandler).toHaveBeenCalled(); + }); + + it('does not call the group users search query', () => { + expect(successGroupSearchQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + beforeEach(() => { + createComponent({ isGroup: true }); + findTokenSelector().vm.$emit('focus'); + findTokenSelector().vm.$emit('text-input', 'jane'); + }); + + it('does not call the project users search query', () => { + expect(successSearchQueryHandler).not.toHaveBeenCalled(); + }); + + it('calls the group users search query', () => { + expect(successGroupSearchQueryHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js index 8b7e04854af..123cf647674 100644 --- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js @@ -20,6 +20,7 @@ describe('WorkItemAttributesWrapper component', () => { const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => { wrapper = shallowMount(WorkItemAttributesWrapper, { propsData: { + fullPath: 'group/project', workItem, }, provide: { @@ -28,7 +29,6 @@ describe('WorkItemAttributesWrapper component', () => { hasOkrsFeature: true, hasIssuableHealthStatusFeature: true, projectNamespace: 'namespace', - fullPath: 'group/project', }, stubs: { WorkItemWeight: true, diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js index f77c5481906..3f14615e173 100644 --- a/spec/frontend/work_items/components/work_item_created_updated_spec.js +++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js @@ -7,12 +7,18 @@ import waitForPromises from 'helpers/wait_for_promises'; import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; -import { workItemByIidResponseFactory, mockAssignees } from '../mock_data'; +import { + groupWorkItemByIidResponseFactory, + mockAssignees, + workItemByIidResponseFactory, +} from '../mock_data'; describe('WorkItemCreatedUpdated component', () => { let wrapper; let successHandler; + let groupSuccessHandler; Vue.use(VueApollo); @@ -30,21 +36,31 @@ describe('WorkItemCreatedUpdated component', () => { updatedAt, confidential = false, updateInProgress = false, + isGroup = false, } = {}) => { - const workItemQueryResponse = workItemByIidResponseFactory({ + const workItemQueryResponse = workItemByIidResponseFactory({ author, updatedAt, confidential }); + const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({ author, updatedAt, confidential, }); successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse); wrapper = shallowMount(WorkItemCreatedUpdated, { - apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]), + apolloProvider: createMockApollo([ + [workItemByIidQuery, successHandler], + [groupWorkItemByIidQuery, groupSuccessHandler], + ]), provide: { + isGroup, + }, + propsData: { fullPath: '/some/project', + workItemIid, + updateInProgress, }, - propsData: { workItemIid, updateInProgress }, stubs: { GlAvatarLink, GlSprintf, @@ -54,10 +70,44 @@ describe('WorkItemCreatedUpdated component', () => { await waitForPromises(); }; - it('skips the work item query when workItemIid is not defined', async () => { - await createComponent({ workItemIid: null }); + describe('when project context', () => { + it('calls the project work item query', async () => { + await createComponent(); + + expect(successHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', async () => { + await createComponent(); + + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); + + it('skips calling the project work item query when workItemIid is not defined', async () => { + await createComponent({ workItemIid: null }); + + expect(successHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', async () => { + await createComponent({ isGroup: true }); + + expect(successHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + await createComponent({ isGroup: true }); - expect(successHandler).not.toHaveBeenCalled(); + expect(groupSuccessHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query when workItemIid is not defined', async () => { + await createComponent({ isGroup: true, workItemIid: null }); + + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); }); it('shows work item type metadata with type and icon', async () => { diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 8b9963b2476..de2895591dd 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -13,9 +13,11 @@ import WorkItemDescription from '~/work_items/components/work_item_description.v import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils'; import { + groupWorkItemByIidResponseFactory, updateWorkItemMutationResponse, workItemByIidResponseFactory, workItemQueryResponse, @@ -33,6 +35,7 @@ describe('WorkItemDescription', () => { const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); let workItemResponseHandler; + let groupWorkItemResponseHandler; const findForm = () => wrapper.findComponent(GlForm); const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); @@ -51,22 +54,28 @@ describe('WorkItemDescription', () => { canUpdate = true, workItemResponse = workItemByIidResponseFactory({ canUpdate }), isEditing = false, + isGroup = false, workItemIid = '1', } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + groupWorkItemResponseHandler = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory({ canUpdate })); const { id } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemDescription, { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemResponseHandler], + [groupWorkItemByIidQuery, groupWorkItemResponseHandler], [updateWorkItemMutation, mutationHandler], ]), propsData: { + fullPath: 'test-project-path', workItemId: id, workItemIid, }, provide: { - fullPath: 'test-project-path', + isGroup, }, }); @@ -247,9 +256,31 @@ describe('WorkItemDescription', () => { }); }); - it('calls the work item query', async () => { - await createComponent(); + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(workItemResponseHandler).toHaveBeenCalled(); + }); - expect(workItemResponseHandler).toHaveBeenCalled(); + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupWorkItemResponseHandler).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index fec6d0673c6..28826748cb0 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -28,12 +28,14 @@ import WorkItemStateToggleButton from '~/work_items/components/work_item_state_t import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; import { i18n } from '~/work_items/constants'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql'; import { + groupWorkItemByIidResponseFactory, mockParent, workItemByIidResponseFactory, objectiveType, @@ -49,6 +51,10 @@ describe('WorkItemDetail component', () => { Vue.use(VueApollo); const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true }); + const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({ + canUpdate: true, + canDelete: true, + }); const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({ canUpdate: false, canDelete: false, @@ -59,6 +65,7 @@ describe('WorkItemDetail component', () => { canDelete: true, }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse); const showModalHandler = jest.fn(); const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0]; const workItemUpdatedSubscriptionHandler = jest @@ -92,6 +99,7 @@ describe('WorkItemDetail component', () => { const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon); const createComponent = ({ + isGroup = false, isModal = false, updateInProgress = false, workItemIid = '1', @@ -101,14 +109,13 @@ describe('WorkItemDetail component', () => { workItemsMvc2Enabled = false, linkedWorkItemsEnabled = false, } = {}) => { - const handlers = [ - [workItemByIidQuery, handler], - [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], - confidentialityMock, - ]; - wrapper = shallowMountExtended(WorkItemDetail, { - apolloProvider: createMockApollo(handlers), + apolloProvider: createMockApollo([ + [workItemByIidQuery, handler], + [groupWorkItemByIidQuery, groupSuccessHandler], + [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], + confidentialityMock, + ]), isLoggedIn: isLoggedIn(), propsData: { isModal, @@ -131,6 +138,7 @@ describe('WorkItemDetail component', () => { hasIssuableHealthStatusFeature: true, projectNamespace: 'namespace', fullPath: 'group/project', + isGroup, reportAbusePath: '/report/abuse/path', }, stubs: { @@ -484,25 +492,64 @@ describe('WorkItemDetail component', () => { expect(findAlert().text()).toBe(updateError); }); - it('calls the work item query', async () => { - createComponent(); - await waitForPromises(); + describe('when project context', () => { + it('calls the project work item query', async () => { + createComponent(); + await waitForPromises(); - expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); - }); + expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); - it('skips the work item query when there is no workItemIid', async () => { - createComponent({ workItemIid: null }); - await waitForPromises(); + it('skips calling the group work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); - expect(successHandler).not.toHaveBeenCalled(); + it('skips calling the project work item query when there is no workItemIid', async () => { + createComponent({ workItemIid: null }); + await waitForPromises(); + + expect(successHandler).not.toHaveBeenCalled(); + }); + + it('calls the project work item query when isModal=true', async () => { + createComponent({ isModal: true }); + await waitForPromises(); + + expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); }); - it('calls the work item query when isModal=true', async () => { - createComponent({ isModal: true }); - await waitForPromises(); + describe('when group context', () => { + it('skips calling the project work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(successHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); + + it('skips calling the group work item query when there is no workItemIid', async () => { + createComponent({ isGroup: true, workItemIid: null }); + await waitForPromises(); - expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query when isModal=true', async () => { + createComponent({ isGroup: true, isModal: true }); + await waitForPromises(); + + expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); }); describe('hierarchy widget', () => { diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index 4a20e654060..28aa7ffa1be 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -7,10 +7,12 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants'; import { + groupWorkItemByIidResponseFactory, projectLabelsResponse, mockLabels, workItemByIidResponseFactory, @@ -32,6 +34,9 @@ describe('WorkItemLabels component', () => { const workItemQuerySuccess = jest .fn() .mockResolvedValue(workItemByIidResponseFactory({ labels: null })); + const groupWorkItemQuerySuccess = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory({ labels: null })); const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse); const successUpdateWorkItemMutationHandler = jest .fn() @@ -40,6 +45,7 @@ describe('WorkItemLabels component', () => { const createComponent = ({ canUpdate = true, + isGroup = false, workItemQueryHandler = workItemQuerySuccess, searchQueryHandler = successSearchQueryHandler, updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, @@ -48,13 +54,15 @@ describe('WorkItemLabels component', () => { wrapper = mountExtended(WorkItemLabels, { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemQueryHandler], + [groupWorkItemByIidQuery, groupWorkItemQuerySuccess], [labelSearchQuery, searchQueryHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], ]), provide: { - fullPath: 'test-project-path', + isGroup, }, propsData: { + fullPath: 'test-project-path', workItemId, workItemIid, canUpdate, @@ -244,17 +252,49 @@ describe('WorkItemLabels component', () => { }); }); - it('calls the work item query', async () => { - createComponent(); - await waitForPromises(); + describe('when project context', () => { + it('calls the project work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(workItemQuerySuccess).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled(); + }); - expect(workItemQuerySuccess).toHaveBeenCalled(); + it('skips calling the project work item query when missing workItemIid', async () => { + createComponent({ workItemIid: '' }); + await waitForPromises(); + + expect(workItemQuerySuccess).not.toHaveBeenCalled(); + }); }); - it('skips calling the work item query when missing workItemIid', async () => { - createComponent({ workItemIid: '' }); - await waitForPromises(); + describe('when group context', () => { + it('skips calling the project work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(workItemQuerySuccess).not.toHaveBeenCalled(); + }); - expect(workItemQuerySuccess).not.toHaveBeenCalled(); + it('calls the group work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(groupWorkItemQuerySuccess).toHaveBeenCalled(); + }); + + it('skips calling the group work item query when missing workItemIid', async () => { + createComponent({ isGroup: true, workItemIid: '' }); + await waitForPromises(); + + expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js index cd077fbf705..0147b199040 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js @@ -53,9 +53,10 @@ describe('WorkItemChildrenWrapper', () => { wrapper = shallowMountExtended(WorkItemChildrenWrapper, { apolloProvider: mockApollo, provide: { - fullPath: 'test/project', + isGroup: false, }, propsData: { + fullPath: 'test/project', workItemType, workItemId: 'gid://gitlab/WorkItem/515', workItemIid: '1', diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index a624bbe8567..9addf6c3450 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -62,9 +62,6 @@ describe('WorkItemLinkChild', () => { [getWorkItemTreeQuery, getWorkItemTreeQueryHandler], [updateWorkItemMutation, mutationChangeParentHandler], ]), - provide: { - fullPath: 'gitlab-org/gitlab-test', - }, propsData: { canUpdate, issuableGid, @@ -93,23 +90,7 @@ describe('WorkItemLinkChild', () => { expect(findWorkItemLinkChildContents().props()).toEqual({ childItem: workItemObjectiveWithChild, canUpdate: true, - childPath: '/gitlab-org/gitlab-test/-/work_items/12', - }); - }); - - describe('with relative instance', () => { - beforeEach(() => { - window.gon = { relative_url_root: '/test' }; - createComponent({ - childItem: workItemObjectiveWithChild, - workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, - }); - }); - - it('adds the relative url to child path value', () => { - expect(findWorkItemLinkChildContents().props('childPath')).toBe( - '/test/gitlab-org/gitlab-test/-/work_items/12', - ); + showTaskIcon: false, }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index aaab22fd18d..0a9da17d284 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -54,6 +54,7 @@ describe('WorkItemLinksForm', () => { [createWorkItemMutation, createMutationResolver], ]), propsData: { + fullPath: 'project/path', issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential, parentIteration, @@ -62,8 +63,8 @@ describe('WorkItemLinksForm', () => { formType, }, provide: { - fullPath: 'project/path', hasIterationsFeature, + isGroup: false, }, }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index e24cfe27616..0b88b3ff5b4 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -13,9 +13,11 @@ import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/wor import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import { FORM_TYPES } from '~/work_items/constants'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { getIssueDetailsResponse, + groupWorkItemByIidResponseFactory, workItemHierarchyResponse, workItemHierarchyEmptyResponse, workItemHierarchyNoUpdatePermissionResponse, @@ -32,6 +34,9 @@ describe('WorkItemLinks', () => { let mockApollo; const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse); + const groupResponseWithAddChildPermission = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory()); const responseWithoutAddChildPermission = jest .fn() .mockResolvedValue(workItemByIidResponseFactory({ adminParentLink: false })); @@ -40,20 +45,22 @@ describe('WorkItemLinks', () => { fetchHandler = responseWithAddChildPermission, issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()), hasIterationsFeature = false, + isGroup = false, } = {}) => { mockApollo = createMockApollo( [ [workItemByIidQuery, fetchHandler], + [groupWorkItemByIidQuery, groupResponseWithAddChildPermission], [issueDetailsQuery, issueDetailsQueryHandler], ], resolvers, - { addTypename: true }, ); wrapper = shallowMountExtended(WorkItemLinks, { provide: { fullPath: 'project/path', hasIterationsFeature, + isGroup, reportAbusePath: '/report/abuse/path', }, propsData: { @@ -243,4 +250,32 @@ describe('WorkItemLinks', () => { expect(findAbuseCategorySelector().exists()).toBe(false); }); }); + + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(responseWithAddChildPermission).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupResponseWithAddChildPermission).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(responseWithAddChildPermission).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupResponseWithAddChildPermission).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 01fa4591cde..f30fded0b45 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -29,10 +29,8 @@ describe('WorkItemTree', () => { canUpdate = true, } = {}) => { wrapper = shallowMountExtended(WorkItemTree, { - provide: { - fullPath: 'test/project', - }, propsData: { + fullPath: 'test/project', workItemType, parentWorkItemType, workItemId: 'gid://gitlab/WorkItem/515', diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js index c42c9a573e5..e303ad4b481 100644 --- a/spec/frontend/work_items/components/work_item_milestone_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_spec.js @@ -66,10 +66,8 @@ describe('WorkItemMilestone component', () => { [projectMilestonesQuery, searchQueryHandler], [updateWorkItemMutation, mutationHandler], ]), - provide: { - fullPath: 'full-path', - }, propsData: { + fullPath: 'full-path', canUpdate, workItemMilestone: milestone, workItemId, diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index 35f01c85ec8..9e02e0708d4 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -98,10 +98,8 @@ describe('WorkItemNotes component', () => { [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler], [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler], ]), - provide: { - fullPath: 'test-path', - }, propsData: { + fullPath: 'test-path', workItemId, workItemIid, workItemType: 'task', diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_spec.js new file mode 100644 index 00000000000..a72eeabc43c --- /dev/null +++ b/spec/frontend/work_items/components/work_item_parent_spec.js @@ -0,0 +1,236 @@ +import * as Sentry from '@sentry/browser'; +import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui'; + +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import WorkItemParent from '~/work_items/components/work_item_parent.vue'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; +import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants'; + +import { + availableObjectivesResponse, + mockParentWidgetResponse, + updateWorkItemMutationResponseFactory, + searchedObjectiveResponse, + updateWorkItemMutationErrorResponse, +} from '../mock_data'; + +jest.mock('@sentry/browser'); + +describe('WorkItemParent component', () => { + Vue.use(VueApollo); + + let wrapper; + + const workItemId = 'gid://gitlab/WorkItem/1'; + const workItemType = 'Objective'; + + const availableWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse); + const availableWorkItemsFailureHandler = jest.fn().mockRejectedValue(new Error()); + + const successUpdateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: mockParentWidgetResponse })); + + const createComponent = ({ + canUpdate = true, + parent = null, + searchQueryHandler = availableWorkItemsSuccessHandler, + mutationHandler = successUpdateWorkItemMutationHandler, + } = {}) => { + wrapper = shallowMountExtended(WorkItemParent, { + apolloProvider: createMockApollo([ + [projectWorkItemsQuery, searchQueryHandler], + [updateWorkItemMutation, mutationHandler], + ]), + provide: { + fullPath: 'full-path', + }, + propsData: { + canUpdate, + parent, + workItemId, + workItemType, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + const findInputGroup = () => wrapper.findComponent(GlFormGroup); + const findParentText = () => wrapper.findByTestId('disabled-text'); + const findCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); + + describe('template', () => { + it('shows field label as Parent', () => { + expect(findInputGroup().exists()).toBe(true); + expect(findInputGroup().attributes('label')).toBe('Parent'); + }); + + it('renders the collapsible listbox with required props', () => { + expect(findCollapsibleListbox().exists()).toBe(true); + expect(findCollapsibleListbox().props()).toMatchObject({ + items: [], + headerText: 'Assign parent', + category: 'tertiary', + loading: false, + noCaret: true, + isCheckCentered: true, + searchable: true, + searching: false, + infiniteScroll: false, + noResultsText: 'No matching results', + toggleText: 'None', + searchPlaceholder: 'Search', + resetButtonLabel: 'Unassign', + block: true, + }); + }); + + it('displays parent text instead of listbox if canUpdate is false', () => { + createComponent({ canUpdate: false, parent: mockParentWidgetResponse }); + + expect(findCollapsibleListbox().exists()).toBe(false); + expect(findParentText().exists()).toBe(true); + expect(findParentText().text()).toBe('Objective 101'); + }); + + it('shows loading while searching', async () => { + await findCollapsibleListbox().vm.$emit('shown'); + expect(findCollapsibleListbox().props('searching')).toBe(true); + expect(findCollapsibleListbox().props('no-caret')).toBeUndefined(); + }); + }); + + describe('work items query', () => { + it('loads work items in the listbox', async () => { + await findCollapsibleListbox().vm.$emit('shown'); + + await waitForPromises(); + + expect(findCollapsibleListbox().props('searching')).toBe(false); + expect(findCollapsibleListbox().props('items')).toStrictEqual([ + { text: 'Objective 101', value: 'gid://gitlab/WorkItem/716' }, + { text: 'Objective 103', value: 'gid://gitlab/WorkItem/712' }, + { text: 'Objective 102', value: 'gid://gitlab/WorkItem/711' }, + ]); + expect(availableWorkItemsSuccessHandler).toHaveBeenCalled(); + }); + + it('emits error when the query fails', async () => { + createComponent({ searchQueryHandler: availableWorkItemsFailureHandler }); + + await findCollapsibleListbox().vm.$emit('shown'); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong while fetching items. Please try again.'], + ]); + }); + + it('searches item when input data is entered', async () => { + const searchedItemQueryHandler = jest.fn().mockResolvedValue(searchedObjectiveResponse); + createComponent({ + searchQueryHandler: searchedItemQueryHandler, + }); + + await findCollapsibleListbox().vm.$emit('shown'); + await findCollapsibleListbox().vm.$emit('search', 'Objective 101'); + + await waitForPromises(); + + expect(searchedItemQueryHandler).toHaveBeenCalledWith({ + fullPath: 'full-path', + searchTerm: 'Objective 101', + types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], + in: 'TITLE', + }); + + await nextTick(); + + expect(findCollapsibleListbox().props('items')).toStrictEqual([ + { text: 'Objective 101', value: 'gid://gitlab/WorkItem/716' }, + ]); + }); + }); + + describe('listbox', () => { + const selectWorkItem = async (workItem) => { + await findCollapsibleListbox().vm.$emit('shown'); + await findCollapsibleListbox().vm.$emit('select', workItem); + }; + + it('calls mutation when item is selected', async () => { + selectWorkItem('gid://gitlab/WorkItem/716'); + + await waitForPromises(); + + expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/716', + }, + }, + }); + }); + + it('calls mutation when item is unassigned', async () => { + const unAssignParentWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: null })); + createComponent({ + mutationHandler: unAssignParentWorkItemMutationHandler, + }); + + await findCollapsibleListbox().vm.$emit('reset'); + + await waitForPromises(); + + expect(unAssignParentWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + hierarchyWidget: { + parentId: null, + }, + }, + }); + }); + + it('emits error when mutation fails', async () => { + createComponent({ + mutationHandler: jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse), + }); + + selectWorkItem('gid://gitlab/WorkItem/716'); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([['Error!']]); + }); + + it('emits error and captures exception in sentry when network request fails', async () => { + const error = new Error('error'); + createComponent({ + mutationHandler: jest.fn().mockRejectedValue(error), + }); + + selectWorkItem('gid://gitlab/WorkItem/716'); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong while updating the objective. Please try again.'], + ]); + expect(Sentry.captureException).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap index 9105e4de5e0..bbc19a011a5 100644 --- a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap +++ b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`WorkItemRelationshipList renders linked item list 1`] = ` -<div> +<div + data-testid="work-item-linked-items-list" +> <h4 class="gl-font-sm gl-font-weight-semibold gl-mb-2 gl-mt-3 gl-mx-2 gl-text-gray-700" data-testid="work-items-list-heading" @@ -20,7 +22,7 @@ exports[`WorkItemRelationshipList renders linked item list 1`] = ` <work-item-link-child-contents-stub canupdate="true" childitem="[object Object]" - childpath="/test-project-path/-/work_items/83" + showtaskicon="true" /> </li> </ul> diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js new file mode 100644 index 00000000000..d7b3ced2ff9 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js @@ -0,0 +1,156 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlForm, GlFormRadioGroup, GlAlert } from '@gitlab/ui'; + +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue'; +import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue'; +import addLinkedItemsMutation from '~/work_items/graphql/add_linked_items.mutation.graphql'; +import { LINKED_ITEM_TYPE_VALUE, MAX_WORK_ITEMS } from '~/work_items/constants'; + +import { linkedWorkItemResponse, generateWorkItemsListWithId } from '../../mock_data'; + +describe('WorkItemAddRelationshipForm', () => { + Vue.use(VueApollo); + + let wrapper; + const linkedWorkItemsSuccessMutationHandler = jest + .fn() + .mockResolvedValue(linkedWorkItemResponse()); + + const createComponent = async ({ + workItemId = 'gid://gitlab/WorkItem/1', + workItemIid = '1', + workItemType = 'Objective', + childrenIds = [], + linkedWorkItemsMutationHandler = linkedWorkItemsSuccessMutationHandler, + } = {}) => { + const mockApolloProvider = createMockApollo([ + [addLinkedItemsMutation, linkedWorkItemsMutationHandler], + ]); + + wrapper = shallowMountExtended(WorkItemAddRelationshipForm, { + apolloProvider: mockApolloProvider, + propsData: { + workItemId, + workItemIid, + workItemFullPath: 'test-project-path', + workItemType, + childrenIds, + }, + }); + + await waitForPromises(); + }; + + const findLinkWorkItemForm = () => wrapper.findComponent(GlForm); + const findLinkWorkItemButton = () => wrapper.findByTestId('link-work-item-button'); + const findMaxWorkItemNote = () => wrapper.findByTestId('max-work-item-note'); + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + const findWorkItemTokenInput = () => wrapper.findComponent(WorkItemTokenInput); + const findGlAlert = () => wrapper.findComponent(GlAlert); + + beforeEach(async () => { + await createComponent(); + }); + + it('renders link work item form with default values', () => { + expect(findLinkWorkItemForm().exists()).toBe(true); + expect(findRadioGroup().props('options')).toEqual([ + { text: 'relates to', value: LINKED_ITEM_TYPE_VALUE.RELATED }, + { text: 'blocks', value: LINKED_ITEM_TYPE_VALUE.BLOCKS }, + { text: 'is blocked by', value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY }, + ]); + expect(findLinkWorkItemButton().attributes('disabled')).toBe('true'); + expect(findMaxWorkItemNote().text()).toBe('Add a maximum of 10 items at a time.'); + }); + + it('renders work item token input with default props', () => { + expect(findWorkItemTokenInput().props()).toMatchObject({ + value: [], + fullPath: 'test-project-path', + childrenIds: [], + parentWorkItemId: 'gid://gitlab/WorkItem/1', + areWorkItemsToAddValid: true, + }); + }); + + describe('linking a work item', () => { + const selectWorkItemTokens = (workItems) => { + findWorkItemTokenInput().vm.$emit('input', workItems); + }; + + it('enables add button when work item is selected', async () => { + await selectWorkItemTokens([ + { + id: 'gid://gitlab/WorkItem/644', + }, + ]); + expect(findLinkWorkItemButton().attributes('disabled')).toBeUndefined(); + }); + + it('disables button when more than 10 work items are selected', async () => { + await selectWorkItemTokens(generateWorkItemsListWithId(MAX_WORK_ITEMS + 1)); + + expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(false); + expect(findLinkWorkItemButton().attributes('disabled')).toBe('true'); + }); + + it.each` + assertionName | linkTypeInput + ${'related'} | ${LINKED_ITEM_TYPE_VALUE.RELATED} + ${'blocking'} | ${LINKED_ITEM_TYPE_VALUE.BLOCKED_BY} + `('selects and links $assertionName work item', async ({ linkTypeInput }) => { + findRadioGroup().vm.$emit('input', linkTypeInput); + await selectWorkItemTokens([ + { + id: 'gid://gitlab/WorkItem/641', + }, + { + id: 'gid://gitlab/WorkItem/642', + }, + ]); + + expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(true); + + findLinkWorkItemForm().vm.$emit('submit', { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }); + await waitForPromises(); + + expect(linkedWorkItemsSuccessMutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + linkType: linkTypeInput, + workItemsIds: ['gid://gitlab/WorkItem/641', 'gid://gitlab/WorkItem/642'], + }, + }); + }); + + it.each` + errorType | mutationMock | errorMessage + ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(linkedWorkItemResponse({}, ['Linked Item failed']))} | ${'Linked Item failed'} + ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('Network Error'))} | ${'Something went wrong when trying to link a item. Please try again.'} + `('shows an error message when there is $errorType', async ({ mutationMock, errorMessage }) => { + createComponent({ linkedWorkItemsMutationHandler: mutationMock }); + await selectWorkItemTokens([ + { + id: 'gid://gitlab/WorkItem/641', + }, + ]); + + findLinkWorkItemForm().vm.$emit('submit', { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }); + await waitForPromises(); + + expect(findGlAlert().exists()).toBe(true); + expect(findGlAlert().text()).toBe(errorMessage); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js index 759ab7e14da..e26bea46ab1 100644 --- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js @@ -14,7 +14,6 @@ describe('WorkItemRelationshipList', () => { linkedItems, heading, canUpdate, - workItemFullPath: 'test-project-path', }, }); }; @@ -35,7 +34,7 @@ describe('WorkItemRelationshipList', () => { expect(findWorkItemLinkChildContents().props()).toMatchObject({ childItem: mockLinkedItems[0].workItem, canUpdate: true, - childPath: '/test-project-path/-/work_items/83', + showTaskIcon: true, }); }); }); diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js index c9a2499b127..7178fa1aae7 100644 --- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js @@ -9,12 +9,17 @@ import waitForPromises from 'helpers/wait_for_promises'; import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue'; import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue'; +import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; +import removeLinkedItemsMutation from '~/work_items/graphql/remove_linked_items.mutation.graphql'; import { + groupWorkItemByIidResponseFactory, workItemByIidResponseFactory, mockLinkedItems, mockBlockingLinkedItem, + removeLinkedWorkItemResponse, } from '../../mock_data'; describe('WorkItemRelationships', () => { @@ -24,23 +29,44 @@ describe('WorkItemRelationships', () => { const emptyLinkedWorkItemsQueryHandler = jest .fn() .mockResolvedValue(workItemByIidResponseFactory()); - const linkedWorkItemsQueryHandler = jest + const groupWorkItemsQueryHandler = jest .fn() - .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })); - const blockingLinkedWorkItemQueryHandler = jest + .mockResolvedValue(groupWorkItemByIidResponseFactory()); + const removeLinkedWorkItemSuccessMutationHandler = jest .fn() - .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem })); + .mockResolvedValue(removeLinkedWorkItemResponse('Successfully unlinked IDs: 2.')); + const removeLinkedWorkItemErrorMutationHandler = jest + .fn() + .mockResolvedValue(removeLinkedWorkItemResponse(null, ['Linked item removal failed'])); + const $toast = { + show: jest.fn(), + }; const createComponent = async ({ workItemQueryHandler = emptyLinkedWorkItemsQueryHandler, + workItemType = 'Task', + isGroup = false, + removeLinkedWorkItemMutationHandler = removeLinkedWorkItemSuccessMutationHandler, } = {}) => { - const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]); + const mockApollo = createMockApollo([ + [workItemByIidQuery, workItemQueryHandler], + [removeLinkedItemsMutation, removeLinkedWorkItemMutationHandler], + [groupWorkItemByIidQuery, groupWorkItemsQueryHandler], + ]); wrapper = shallowMountExtended(WorkItemRelationships, { apolloProvider: mockApollo, propsData: { + workItemId: 'gid://gitlab/WorkItem/1', workItemIid: '1', workItemFullPath: 'test-project-path', + workItemType, + }, + provide: { + isGroup, + }, + mocks: { + $toast, }, }); @@ -51,8 +77,11 @@ describe('WorkItemRelationships', () => { const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper); const findEmptyRelatedMessageContainer = () => wrapper.findByTestId('links-empty'); const findLinkedItemsCountContainer = () => wrapper.findByTestId('linked-items-count'); + const findLinkedItemsHelpLink = () => wrapper.findByTestId('help-link'); const findAllWorkItemRelationshipListComponents = () => wrapper.findAllComponents(WorkItemRelationshipList); + const findAddButton = () => wrapper.findByTestId('link-item-add-button'); + const findWorkItemRelationshipForm = () => wrapper.findComponent(WorkItemAddRelationshipForm); it('shows loading icon when query is not processed', () => { createComponent(); @@ -60,22 +89,35 @@ describe('WorkItemRelationships', () => { expect(findLoadingIcon().exists()).toBe(true); }); - it('renders the component with empty message when there are no items', async () => { + it('renders the component with with defaults', async () => { await createComponent(); expect(wrapper.find('.work-item-relationships').exists()).toBe(true); expect(findEmptyRelatedMessageContainer().exists()).toBe(true); + expect(findAddButton().exists()).toBe(true); + expect(findWorkItemRelationshipForm().exists()).toBe(false); + expect(findLinkedItemsHelpLink().attributes('href')).toBe( + '/help/user/okrs.md#linked-items-in-okrs', + ); }); it('renders blocking linked item lists', async () => { - await createComponent({ workItemQueryHandler: blockingLinkedWorkItemQueryHandler }); + await createComponent({ + workItemQueryHandler: jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem })), + }); expect(findAllWorkItemRelationshipListComponents().length).toBe(1); expect(findLinkedItemsCountContainer().text()).toBe('1'); }); it('renders blocking, blocked by and related to linked item lists with proper count', async () => { - await createComponent({ workItemQueryHandler: linkedWorkItemsQueryHandler }); + await createComponent({ + workItemQueryHandler: jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })), + }); // renders all 3 lists: blocking, blocked by and related to expect(findAllWorkItemRelationshipListComponents().length).toBe(3); @@ -90,4 +132,103 @@ describe('WorkItemRelationships', () => { expect(findWidgetWrapper().props('error')).toBe(errorMessage); }); + + it('does not render add button when there is no permission', async () => { + await createComponent({ + workItemQueryHandler: jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ canAdminWorkItemLink: false })), + }); + + expect(findAddButton().exists()).toBe(false); + }); + + it('shows form on add button and hides when cancel button is clicked', async () => { + await createComponent(); + + await findAddButton().vm.$emit('click'); + expect(findWorkItemRelationshipForm().exists()).toBe(true); + + await findWorkItemRelationshipForm().vm.$emit('cancel'); + expect(findWorkItemRelationshipForm().exists()).toBe(false); + }); + + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(emptyLinkedWorkItemsQueryHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupWorkItemsQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(emptyLinkedWorkItemsQueryHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupWorkItemsQueryHandler).toHaveBeenCalled(); + }); + }); + + it('removes linked item and shows toast message when removeLinkedItem event is emitted', async () => { + await createComponent({ + workItemQueryHandler: jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })), + }); + + expect(findLinkedItemsCountContainer().text()).toBe('3'); + + await findAllWorkItemRelationshipListComponents() + .at(0) + .vm.$emit('removeLinkedItem', { id: 'gid://gitlab/WorkItem/2' }); + + await waitForPromises(); + + expect(removeLinkedWorkItemSuccessMutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + workItemsIds: ['gid://gitlab/WorkItem/2'], + }, + }); + + expect($toast.show).toHaveBeenCalledWith('Linked item removed'); + + expect(findLinkedItemsCountContainer().text()).toBe('2'); + }); + + it.each` + errorType | mutationMock | errorMessage + ${'an error in the mutation response'} | ${removeLinkedWorkItemErrorMutationHandler} | ${'Linked item removal failed'} + ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('Network Error'))} | ${'Something went wrong when removing item. Please refresh this page.'} + `( + 'shows an error message when there is $errorType while removing items', + async ({ mutationMock, errorMessage }) => { + await createComponent({ + workItemQueryHandler: jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })), + removeLinkedWorkItemMutationHandler: mutationMock, + }); + + await findAllWorkItemRelationshipListComponents() + .at(0) + .vm.$emit('removeLinkedItem', { id: 'gid://gitlab/WorkItem/2' }); + + await waitForPromises(); + + expect(findWidgetWrapper().props('error')).toBe(errorMessage); + }, + ); }); diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js index 454bd97bbee..c76cdbcee53 100644 --- a/spec/frontend/work_items/components/work_item_todos_spec.js +++ b/spec/frontend/work_items/components/work_item_todos_spec.js @@ -86,6 +86,9 @@ describe('WorkItemTodo component', () => { workItemFullpath: mockWorkItemFullpath, currentUserTodos, }, + provide: { + isGroup: false, + }, }); }; |