diff options
Diffstat (limited to 'spec/frontend/work_items')
32 files changed, 1151 insertions, 238 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, + }, }); }; diff --git a/spec/frontend/work_items/graphql/cache_utils_spec.js b/spec/frontend/work_items/graphql/cache_utils_spec.js index 6d0083790d1..64ef1bdbb88 100644 --- a/spec/frontend/work_items/graphql/cache_utils_spec.js +++ b/spec/frontend/work_items/graphql/cache_utils_spec.js @@ -43,7 +43,7 @@ describe('work items graphql cache utils', () => { title: 'New child', }; - addHierarchyChild(mockCache, fullPath, iid, child); + addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child }); expect(mockCache.writeQuery).toHaveBeenCalledWith({ query: workItemByIidQuery, @@ -88,7 +88,7 @@ describe('work items graphql cache utils', () => { title: 'New child', }; - addHierarchyChild(mockCache, fullPath, iid, child); + addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child }); expect(mockCache.writeQuery).not.toHaveBeenCalled(); }); @@ -106,7 +106,7 @@ describe('work items graphql cache utils', () => { title: 'Child', }; - removeHierarchyChild(mockCache, fullPath, iid, childToRemove); + removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove }); expect(mockCache.writeQuery).toHaveBeenCalledWith({ query: workItemByIidQuery, @@ -145,7 +145,7 @@ describe('work items graphql cache utils', () => { title: 'Child', }; - removeHierarchyChild(mockCache, fullPath, iid, childToRemove); + removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove }); expect(mockCache.writeQuery).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index ba244b19eb5..9eb604c81cb 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -146,6 +146,7 @@ export const workItemQueryResponse = { setWorkItemMetadata: false, adminParentLink: false, createNote: false, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, widgets: [ @@ -193,6 +194,7 @@ export const workItemQueryResponse = { confidential: false, title: '123', state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/4', workItemType: { id: '1', name: 'Task', @@ -251,6 +253,7 @@ export const updateWorkItemMutationResponse = { setWorkItemMetadata: false, adminParentLink: false, createNote: false, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, reference: 'test-project-path#1', @@ -269,6 +272,7 @@ export const updateWorkItemMutationResponse = { confidential: false, title: '123', state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/4', workItemType: { id: '1', name: 'Task', @@ -360,6 +364,7 @@ export const convertWorkItemMutationResponse = { setWorkItemMetadata: false, adminParentLink: false, createNote: false, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, reference: 'gitlab-org/gitlab-test#1', @@ -378,6 +383,7 @@ export const convertWorkItemMutationResponse = { confidential: false, title: '123', state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/4', workItemType: { id: '1', name: 'Task', @@ -486,6 +492,7 @@ export const mockBlockingLinkedItem = { state: 'OPEN', createdAt: '2023-03-28T10:50:16Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/83', widgets: [], __typename: 'WorkItem', }, @@ -518,6 +525,7 @@ export const mockLinkedItems = { state: 'OPEN', createdAt: '2023-03-28T10:50:16Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/83', widgets: [], __typename: 'WorkItem', }, @@ -540,6 +548,7 @@ export const mockLinkedItems = { state: 'OPEN', createdAt: '2023-03-28T10:50:16Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/55', widgets: [], __typename: 'WorkItem', }, @@ -562,6 +571,7 @@ export const mockLinkedItems = { state: 'OPEN', createdAt: '2023-03-28T10:50:16Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/56', widgets: [], __typename: 'WorkItem', }, @@ -579,6 +589,7 @@ export const workItemResponseFactory = ({ canDelete = false, canCreateNote = false, adminParentLink = false, + canAdminWorkItemLink = true, notificationsWidgetPresent = true, currentUserTodosWidgetPresent = true, awardEmojiWidgetPresent = true, @@ -636,6 +647,7 @@ export const workItemResponseFactory = ({ updateWorkItem: canUpdate, setWorkItemMetadata: canUpdate, adminParentLink, + adminWorkItemLink: canAdminWorkItemLink, createNote: canCreateNote, __typename: 'WorkItemPermissions', }, @@ -756,6 +768,7 @@ export const workItemResponseFactory = ({ confidential: false, title: '123', state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/5', workItemType: { id: '1', name: 'Task', @@ -828,13 +841,16 @@ export const workItemByIidResponseFactory = (options) => { }; }; -export const updateWorkItemMutationResponseFactory = (options) => { +export const groupWorkItemByIidResponseFactory = (options) => { const response = workItemResponseFactory(options); return { data: { - workItemUpdate: { - workItem: response.data.workItem, - errors: [], + workspace: { + __typename: 'Group', + id: 'gid://gitlab/Group/1', + workItems: { + nodes: [response.data.workItem], + }, }, }, }; @@ -914,6 +930,7 @@ export const createWorkItemMutationResponse = { setWorkItemMetadata: false, adminParentLink: false, createNote: false, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, reference: 'test-project-path#1', @@ -996,6 +1013,7 @@ export const workItemHierarchyEmptyResponse = { setWorkItemMetadata: false, adminParentLink: false, createNote: false, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, confidential: false, @@ -1046,6 +1064,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { setWorkItemMetadata: false, adminParentLink: false, createNote: false, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, project: { @@ -1077,6 +1096,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/2', widgets: [ { type: 'HIERARCHY', @@ -1110,6 +1130,7 @@ export const workItemTask = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/4', widgets: [], __typename: 'WorkItem', }; @@ -1128,6 +1149,7 @@ export const confidentialWorkItemTask = { confidential: true, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/2', widgets: [], __typename: 'WorkItem', }; @@ -1146,6 +1168,7 @@ export const closedWorkItemTask = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: '2022-08-12T13:07:52Z', + webUrl: '/gitlab-org/gitlab-test/-/work_items/3', widgets: [], __typename: 'WorkItem', }; @@ -1168,6 +1191,7 @@ export const childrenWorkItems = [ confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/5', widgets: [], __typename: 'WorkItem', }, @@ -1196,6 +1220,7 @@ export const workItemHierarchyResponse = { setWorkItemMetadata: true, adminParentLink: true, createNote: true, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, author: { @@ -1297,6 +1322,7 @@ export const workItemObjectiveWithChild = { setWorkItemMetadata: true, adminParentLink: true, createNote: true, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, author: { @@ -1368,6 +1394,7 @@ export const workItemHierarchyTreeResponse = { setWorkItemMetadata: true, adminParentLink: true, createNote: true, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, confidential: false, @@ -1403,6 +1430,7 @@ export const workItemHierarchyTreeResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + webUrl: '/gitlab-org/gitlab-test/-/work_items/13', widgets: [ { type: 'HIERARCHY', @@ -1449,6 +1477,7 @@ export const changeIndirectWorkItemParentMutationResponse = { setWorkItemMetadata: true, adminParentLink: true, createNote: true, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, description: null, @@ -1517,6 +1546,7 @@ export const changeWorkItemParentMutationResponse = { setWorkItemMetadata: true, adminParentLink: true, createNote: true, + adminWorkItemLink: true, __typename: 'WorkItemPermissions', }, description: null, @@ -1568,6 +1598,7 @@ export const availableWorkItemsResponse = { nodes: [ { id: 'gid://gitlab/WorkItem/458', + iid: '2', title: 'Task 1', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', @@ -1576,6 +1607,7 @@ export const availableWorkItemsResponse = { }, { id: 'gid://gitlab/WorkItem/459', + iid: '3', title: 'Task 2', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', @@ -1584,6 +1616,7 @@ export const availableWorkItemsResponse = { }, { id: 'gid://gitlab/WorkItem/460', + iid: '4', title: 'Task 3', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', @@ -1596,6 +1629,64 @@ export const availableWorkItemsResponse = { }, }; +export const availableObjectivesResponse = { + data: { + workspace: { + __typename: 'Project', + id: 'gid://gitlab/Project/2', + workItems: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/716', + iid: '122', + title: 'Objective 101', + state: 'OPEN', + confidential: false, + __typename: 'WorkItem', + }, + { + id: 'gid://gitlab/WorkItem/712', + iid: '118', + title: 'Objective 103', + state: 'OPEN', + confidential: false, + __typename: 'WorkItem', + }, + { + id: 'gid://gitlab/WorkItem/711', + iid: '117', + title: 'Objective 102', + state: 'OPEN', + confidential: false, + __typename: 'WorkItem', + }, + ], + }, + }, + }, +}; + +export const searchedObjectiveResponse = { + data: { + workspace: { + __typename: 'Project', + id: 'gid://gitlab/Project/2', + workItems: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/716', + iid: '122', + title: 'Objective 101', + state: 'OPEN', + confidential: false, + __typename: 'WorkItem', + }, + ], + }, + }, + }, +}; + export const searchedWorkItemsResponse = { data: { workspace: { @@ -1605,6 +1696,7 @@ export const searchedWorkItemsResponse = { nodes: [ { id: 'gid://gitlab/WorkItem/459', + iid: '3', title: 'Task 2', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', @@ -1931,6 +2023,21 @@ export const mockMilestoneWidgetResponse = { title: 'v4.0', }; +export const mockParentWidgetResponse = { + id: 'gid://gitlab/WorkItem/716', + iid: '122', + title: 'Objective 101', + confidential: false, + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-test/-/work_items/122', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + __typename: 'WorkItem', +}; + export const projectMilestonesResponse = { data: { workspace: { @@ -3439,6 +3546,31 @@ export const getTodosMutationResponse = (state) => { }; }; +export const linkedWorkItemResponse = (options, errors = []) => { + const response = workItemResponseFactory(options); + return { + data: { + workItemAddLinkedItems: { + workItem: response.data.workItem, + errors, + __typename: 'WorkItemAddLinkedItemsPayload', + }, + }, + }; +}; + +export const removeLinkedWorkItemResponse = (message, errors = []) => { + return { + data: { + workItemRemoveLinkedItems: { + errors, + message, + __typename: 'WorkItemRemoveLinkedItemsPayload', + }, + }, + }; +}; + export const groupWorkItemsQueryResponse = { data: { group: { @@ -3498,3 +3630,36 @@ export const groupWorkItemsQueryResponse = { }, }, }; + +export const updateWorkItemMutationResponseFactory = (options) => { + const response = workItemResponseFactory(options); + return { + data: { + workItemUpdate: { + workItem: response.data.workItem, + errors: [], + }, + }, + }; +}; + +export const updateWorkItemNotificationsMutationResponse = (subscribed) => ({ + data: { + workItemSubscribe: { + workItem: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetNotifications', + type: 'NOTIFICATIONS', + subscribed, + }, + ], + }, + errors: [], + }, + }, +}); + +export const generateWorkItemsListWithId = (count) => + Array.from({ length: count }, (_, i) => ({ id: `gid://gitlab/WorkItem/${i + 1}` })); diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index c369a454286..527f5890338 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -65,6 +65,7 @@ describe('Create work item component', () => { }, provide: { fullPath: 'full-path', + isGroup: false, }, }); }; @@ -199,8 +200,6 @@ describe('Create work item component', () => { wrapper.find('form').trigger('submit'); await waitForPromises(); - expect(findAlert().text()).toBe( - 'Something went wrong when creating work item. Please try again.', - ); + expect(findAlert().text()).toBe('Something went wrong when creating item. Please try again.'); }); }); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 79ba31e7012..d4efcf78189 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -41,6 +41,7 @@ describe('Work items router', () => { router, provide: { fullPath: 'full-path', + isGroup: false, issuesListPath: 'full-path/-/issues', hasIssueWeightsFeature: false, hasIterationsFeature: false, diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js index 8a49140119d..aa24b80cf08 100644 --- a/spec/frontend/work_items/utils_spec.js +++ b/spec/frontend/work_items/utils_spec.js @@ -1,4 +1,4 @@ -import { autocompleteDataSources, markdownPreviewPath, workItemPath } from '~/work_items/utils'; +import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils'; describe('autocompleteDataSources', () => { beforeEach(() => { @@ -25,14 +25,3 @@ describe('markdownPreviewPath', () => { ); }); }); - -describe('workItemPath', () => { - it('returns corrrect data sources', () => { - expect(workItemPath('project/group', '2')).toEqual('/project/group/-/work_items/2'); - }); - - it('returns corrrect data sources with relative url root', () => { - gon.relative_url_root = '/foobar'; - expect(workItemPath('project/group', '2')).toEqual('/foobar/project/group/-/work_items/2'); - }); -}); |