diff options
Diffstat (limited to 'spec/frontend/work_items')
23 files changed, 1287 insertions, 271 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index 3a84ba4bd5e..660ff671a80 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -2,11 +2,12 @@ import { shallowMount } from '@vue/test-utils'; import { escape } from 'lodash'; import ItemTitle from '~/work_items/components/item_title.vue'; -const createComponent = ({ title = 'Sample title', disabled = false } = {}) => +const createComponent = ({ title = 'Sample title', disabled = false, useH1 = false } = {}) => shallowMount(ItemTitle, { propsData: { title, disabled, + useH1, }, }); @@ -27,6 +28,12 @@ describe('ItemTitle', () => { expect(findInputEl().text()).toBe('Sample title'); }); + it('renders H1 if useH1 is true, otherwise renders H2', () => { + expect(wrapper.element.tagName).toBe('H2'); + wrapper = createComponent({ useH1: true }); + expect(wrapper.element.tagName).toBe('H1'); + }); + it('renders title contents with editing disabled', () => { wrapper = createComponent({ disabled: true, 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 596283a9590..97aed1d548e 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 @@ -1,4 +1,4 @@ -import { GlButton, GlDisclosureDropdown } from '@gitlab/ui'; +import { GlDisclosureDropdown } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -17,7 +17,7 @@ describe('Work Item Note Actions', () => { const showSpy = jest.fn(); const findReplyButton = () => wrapper.findComponent(ReplyButton); - const findEditButton = () => wrapper.findComponent(GlButton); + const findEditButton = () => wrapper.findByTestId('note-actions-edit'); const findEmojiButton = () => wrapper.findByTestId('note-emoji-button'); const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action'); @@ -64,6 +64,7 @@ describe('Work Item Note Actions', () => { projectName, }, provide: { + isGroup: false, 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 ce915635946..6ce4c09329f 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 @@ -9,6 +9,7 @@ import AwardsList from '~/vue_shared/components/awards_list.vue'; import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue'; import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; +import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import { mockWorkItemNotesResponseWithComments, @@ -45,7 +46,9 @@ describe('Work Item Note Awards List', () => { const findAwardsList = () => wrapper.findComponent(AwardsList); const createComponent = ({ + isGroup = false, note = firstNote, + query = workItemNotesByIidQuery, addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler, removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler, } = {}) => { @@ -55,12 +58,15 @@ describe('Work Item Note Awards List', () => { ]); apolloProvider.clients.defaultClient.writeQuery({ - query: workItemNotesByIidQuery, + query, variables: { fullPath, iid: workItemIid }, ...mockWorkItemNotesResponseWithComments, }); wrapper = shallowMount(WorkItemNoteAwardsList, { + provide: { + isGroup, + }, propsData: { fullPath, workItemIid, @@ -89,54 +95,58 @@ describe('Work Item Note Awards List', () => { expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission); }); - it('adds award if not already awarded', async () => { - createComponent(); - await waitForPromises(); - - findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); - - expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({ - awardableId: firstNote.id, - name: EMOJI_THUMBSUP, - }); - }); + it.each` + isGroup | query + ${true} | ${groupWorkItemNotesByIidQuery} + ${false} | ${workItemNotesByIidQuery} + `( + 'adds award if not already awarded in both group and project contexts', + async ({ isGroup, query }) => { + createComponent({ isGroup, query }); + await waitForPromises(); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); + + expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({ + awardableId: firstNote.id, + name: EMOJI_THUMBSUP, + }); + }, + ); it('emits error if awarding emoji fails', async () => { - createComponent({ - addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'), - }); - await waitForPromises(); + createComponent({ addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no') }); findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); - await waitForPromises(); expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]); }); - it('removes award if already awarded', async () => { - const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler; - - createComponent({ removeAwardEmojiMutationHandler }); - - findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); - - await waitForPromises(); - - expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({ - awardableId: firstNote.id, - name: EMOJI_THUMBSDOWN, - }); - }); + it.each` + isGroup | query + ${true} | ${groupWorkItemNotesByIidQuery} + ${false} | ${workItemNotesByIidQuery} + `( + 'removes award if already awarded in both group and project contexts', + async ({ isGroup, query }) => { + const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler; + createComponent({ isGroup, query, removeAwardEmojiMutationHandler }); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); + await waitForPromises(); + + expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({ + awardableId: firstNote.id, + name: EMOJI_THUMBSDOWN, + }); + }, + ); it('restores award if remove fails', async () => { - createComponent({ - removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'), - }); - await waitForPromises(); + createComponent({ removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no') }); findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); - await waitForPromises(); expect(wrapper.emitted('error')).toEqual([[__('Failed to remove emoji. Please try again')]]); diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js index daf74f7a93b..dff54fef9fe 100644 --- a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js @@ -9,7 +9,8 @@ import { describe('Work Item Note Activity Header', () => { let wrapper; - const findActivityLabelHeading = () => wrapper.find('h3'); + const findActivityLabelH2Heading = () => wrapper.find('h2'); + const findActivityLabelH3Heading = () => wrapper.find('h3'); const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter'); const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort'); @@ -18,6 +19,7 @@ describe('Work Item Note Activity Header', () => { sortOrder = ASC, workItemType = 'Task', discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES, + useH2 = false, } = {}) => { wrapper = shallowMountExtended(WorkItemNotesActivityHeader, { propsData: { @@ -25,6 +27,7 @@ describe('Work Item Note Activity Header', () => { sortOrder, workItemType, discussionFilter, + useH2, }, }); }; @@ -34,7 +37,18 @@ describe('Work Item Note Activity Header', () => { }); it('Should have the Activity label', () => { - expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel); + expect(findActivityLabelH3Heading().text()).toBe( + WorkItemNotesActivityHeader.i18n.activityLabel, + ); + }); + + it('Should render an H2 instead of an H3 if useH2 is true', () => { + createComponent(); + expect(findActivityLabelH3Heading().exists()).toBe(true); + expect(findActivityLabelH2Heading().exists()).toBe(false); + createComponent({ useH2: true }); + expect(findActivityLabelH2Heading().exists()).toBe(true); + expect(findActivityLabelH3Heading().exists()).toBe(false); }); it('Should have Activity filtering dropdown', () => { diff --git a/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js new file mode 100644 index 00000000000..2cfe61654ad --- /dev/null +++ b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js @@ -0,0 +1,53 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import DisclosureHierarchyItem from '~/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue'; +import { mockDisclosureHierarchyItems } from './mock_data'; + +describe('DisclosurePathItem', () => { + let wrapper; + + const findIcon = () => wrapper.findComponent(GlIcon); + + const createComponent = (props = {}, options = {}) => { + return shallowMount(DisclosureHierarchyItem, { + propsData: { + item: mockDisclosureHierarchyItems[0], + ...props, + }, + ...options, + }); + }; + + beforeEach(() => { + wrapper = createComponent(); + }); + + describe('renders the item', () => { + it('renders the inline icon', () => { + expect(findIcon().exists()).toBe(true); + expect(findIcon().props('name')).toBe(mockDisclosureHierarchyItems[0].icon); + }); + }); + + describe('item slot', () => { + beforeEach(() => { + wrapper = createComponent(null, { + scopedSlots: { + default: ` + <div + data-testid="item-slot-content"> + {{ props.item.title }} + </div> + `, + }, + }); + }); + + it('contains all elements passed into the additional slot', () => { + const item = wrapper.find('[data-testid="item-slot-content"]'); + + expect(item.text()).toBe(mockDisclosureHierarchyItems[0].title); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js new file mode 100644 index 00000000000..b808c13c3e7 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js @@ -0,0 +1,99 @@ +import { shallowMount } from '@vue/test-utils'; + +import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui'; +import DisclosureHierarchy from '~/work_items/components/work_item_ancestors//disclosure_hierarchy.vue'; +import DisclosureHierarchyItem from '~/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue'; +import { mockDisclosureHierarchyItems } from './mock_data'; + +describe('DisclosurePath', () => { + let wrapper; + + const createComponent = (props = {}, options = {}) => { + return shallowMount(DisclosureHierarchy, { + propsData: { + items: mockDisclosureHierarchyItems, + ...props, + }, + ...options, + }); + }; + + const listItems = () => wrapper.findAllComponents(DisclosureHierarchyItem); + const itemAt = (index) => listItems().at(index); + const itemTextAt = (index) => itemAt(index).props('item').title; + + beforeEach(() => { + wrapper = createComponent(); + }); + + describe('renders the list of items', () => { + it('renders the correct number of items', () => { + expect(listItems().length).toBe(mockDisclosureHierarchyItems.length); + }); + + it('renders the items in the correct order', () => { + expect(itemTextAt(0)).toContain(mockDisclosureHierarchyItems[0].title); + expect(itemTextAt(4)).toContain(mockDisclosureHierarchyItems[4].title); + expect(itemTextAt(9)).toContain(mockDisclosureHierarchyItems[9].title); + }); + }); + + describe('slots', () => { + beforeEach(() => { + wrapper = createComponent(null, { + scopedSlots: { + default: ` + <div + :data-itemid="props.itemId" + data-testid="item-slot-content"> + {{ props.item.title }} + </div> + `, + }, + }); + }); + + it('contains all elements passed into the default slot', () => { + mockDisclosureHierarchyItems.forEach((item, index) => { + const disclosureItem = wrapper.findAll('[data-testid="item-slot-content"]').at(index); + + expect(disclosureItem.text()).toBe(item.title); + expect(disclosureItem.attributes('data-itemid')).toContain('disclosure-'); + }); + }); + }); + + describe('with ellipsis', () => { + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findTooltip = () => wrapper.findComponent(GlTooltip); + const findTooltipText = () => findTooltip().text(); + const tooltipText = 'Display more items'; + + beforeEach(() => { + wrapper = createComponent({ withEllipsis: true, ellipsisTooltipLabel: tooltipText }); + }); + + describe('renders items and dropdown', () => { + it('renders 2 items', () => { + expect(listItems().length).toBe(2); + }); + + it('renders first and last items', () => { + expect(itemTextAt(0)).toContain(mockDisclosureHierarchyItems[0].title); + expect(itemTextAt(1)).toContain( + mockDisclosureHierarchyItems[mockDisclosureHierarchyItems.length - 1].title, + ); + }); + + it('renders dropdown with the rest of the items passed down', () => { + expect(findDropdown().exists()).toBe(true); + expect(findDropdown().props('items').length).toBe(mockDisclosureHierarchyItems.length - 2); + }); + + it('renders tooltip with text passed as prop', () => { + expect(findTooltip().exists()).toBe(true); + expect(findTooltipText()).toBe(tooltipText); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_ancestors/mock_data.js b/spec/frontend/work_items/components/work_item_ancestors/mock_data.js new file mode 100644 index 00000000000..8e7f99658de --- /dev/null +++ b/spec/frontend/work_items/components/work_item_ancestors/mock_data.js @@ -0,0 +1,197 @@ +export const mockDisclosureHierarchyItems = [ + { + title: 'First', + icon: 'epic', + href: '#', + }, + { + title: 'Second', + icon: 'epic', + href: '#', + }, + { + title: 'Third', + icon: 'epic', + href: '#', + }, + { + title: 'Fourth', + icon: 'epic', + href: '#', + }, + { + title: 'Fifth', + icon: 'issues', + href: '#', + }, + { + title: 'Sixth', + icon: 'issues', + href: '#', + }, + { + title: 'Seventh', + icon: 'issues', + href: '#', + }, + { + title: 'Eighth', + icon: 'issue-type-task', + href: '#', + disabled: true, + }, + { + title: 'Ninth', + icon: 'issue-type-task', + href: '#', + }, + { + title: 'Tenth', + icon: 'issue-type-task', + href: '#', + }, +]; + +export const workItemAncestorsQueryResponse = { + data: { + workItem: { + __typename: 'WorkItem', + id: 'gid://gitlab/WorkItem/1', + title: 'Test', + widgets: [ + { + __typename: 'WorkItemWidgetHierarchy', + type: 'HIERARCHY', + parent: { + id: 'gid://gitlab/Issue/1', + }, + ancestors: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/444', + iid: '4', + reference: '#40', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + confidential: false, + title: '123', + state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/4', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2', + name: 'Issue', + iconName: 'issue-type-issue', + }, + }, + ], + }, + }, + ], + }, + }, +}; + +export const workItemThreeAncestorsQueryResponse = { + data: { + workItem: { + __typename: 'WorkItem', + id: 'gid://gitlab/WorkItem/1', + title: 'Test', + workItemType: { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', + }, + widgets: [ + { + __typename: 'WorkItemWidgetHierarchy', + type: 'HIERARCHY', + parent: { + id: 'gid://gitlab/Issue/1', + }, + ancestors: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/444', + iid: '4', + reference: '#40', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + confidential: false, + title: '123', + state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/4', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2', + name: 'Issue', + iconName: 'issue-type-issue', + }, + }, + { + id: 'gid://gitlab/WorkItem/445', + iid: '5', + reference: '#41', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + confidential: false, + title: '1234', + state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/5', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2', + name: 'Issue', + iconName: 'issue-type-issue', + }, + }, + { + id: 'gid://gitlab/WorkItem/446', + iid: '6', + reference: '#42', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + confidential: false, + title: '12345', + state: 'OPEN', + webUrl: '/gitlab-org/gitlab-test/-/work_items/6', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2', + name: 'Issue', + iconName: 'issue-type-issue', + }, + }, + ], + }, + }, + ], + }, + }, +}; + +export const workItemEmptyAncestorsQueryResponse = { + data: { + workItem: { + __typename: 'WorkItem', + id: 'gid://gitlab/WorkItem/1', + title: 'Test', + workItemType: { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', + }, + widgets: [ + { + __typename: 'WorkItemWidgetHierarchy', + type: 'HIERARCHY', + parent: { + id: null, + }, + ancestors: { + nodes: [], + }, + }, + ], + }, + }, +}; diff --git a/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js b/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js new file mode 100644 index 00000000000..a9f66b20f06 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlPopover } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +import { createAlert } from '~/alert'; +import DisclosureHierarchy from '~/work_items/components/work_item_ancestors/disclosure_hierarchy.vue'; +import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue'; +import workItemAncestorsQuery from '~/work_items/graphql/work_item_ancestors.query.graphql'; +import { formatAncestors } from '~/work_items/utils'; + +import { workItemTask } from '../../mock_data'; +import { + workItemAncestorsQueryResponse, + workItemEmptyAncestorsQueryResponse, + workItemThreeAncestorsQueryResponse, +} from './mock_data'; + +Vue.use(VueApollo); +jest.mock('~/alert'); + +describe('WorkItemAncestors', () => { + let wrapper; + let mockApollo; + + const workItemAncestorsQueryHandler = jest.fn().mockResolvedValue(workItemAncestorsQueryResponse); + const workItemEmptyAncestorsQueryHandler = jest + .fn() + .mockResolvedValue(workItemEmptyAncestorsQueryResponse); + const workItemThreeAncestorsQueryHandler = jest + .fn() + .mockResolvedValue(workItemThreeAncestorsQueryResponse); + const workItemAncestorsFailureHandler = jest.fn().mockRejectedValue(new Error()); + + const findDisclosureHierarchy = () => wrapper.findComponent(DisclosureHierarchy); + const findPopover = () => wrapper.findComponent(GlPopover); + + const createComponent = ({ + props = {}, + options = {}, + ancestorsQueryHandler = workItemAncestorsQueryHandler, + } = {}) => { + mockApollo = createMockApollo([[workItemAncestorsQuery, ancestorsQueryHandler]]); + return mountExtended(WorkItemAncestors, { + apolloProvider: mockApollo, + propsData: { + workItem: workItemTask, + ...props, + }, + ...options, + }); + }; + + beforeEach(async () => { + createAlert.mockClear(); + wrapper = createComponent(); + await waitForPromises(); + }); + + it('fetches work item ancestors', () => { + expect(workItemAncestorsQueryHandler).toHaveBeenCalled(); + }); + + it('displays DisclosureHierarchy component with ancestors when work item has at least one ancestor', () => { + expect(findDisclosureHierarchy().exists()).toBe(true); + expect(findDisclosureHierarchy().props('items')).toEqual( + expect.objectContaining(formatAncestors(workItemAncestorsQueryResponse.data.workItem)), + ); + }); + + it('does not display DisclosureHierarchy component when work item has no ancestor', async () => { + wrapper = createComponent({ ancestorsQueryHandler: workItemEmptyAncestorsQueryHandler }); + await waitForPromises(); + + expect(findDisclosureHierarchy().exists()).toBe(false); + }); + + it('displays work item info in popover on hover and focus', () => { + expect(findPopover().exists()).toBe(true); + expect(findPopover().props('triggers')).toBe('hover focus'); + + const ancestor = findDisclosureHierarchy().props('items')[0]; + + expect(findPopover().text()).toContain(ancestor.title); + expect(findPopover().text()).toContain(ancestor.reference); + }); + + describe('when work item has less than 3 ancestors', () => { + it('does not activate ellipsis option for DisclosureHierarchy component', () => { + expect(findDisclosureHierarchy().props('withEllipsis')).toBe(false); + }); + }); + + describe('when work item has at least 3 ancestors', () => { + beforeEach(async () => { + wrapper = createComponent({ ancestorsQueryHandler: workItemThreeAncestorsQueryHandler }); + await waitForPromises(); + }); + + it('activates ellipsis option for DisclosureHierarchy component', () => { + expect(findDisclosureHierarchy().props('withEllipsis')).toBe(true); + }); + }); + + it('creates alert when the query fails', async () => { + createComponent({ ancestorsQueryHandler: workItemAncestorsFailureHandler }); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + captureError: true, + error: expect.any(Object), + message: 'Something went wrong while fetching ancestors.', + }); + }); +}); 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 123cf647674..48ec84ceb85 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 @@ -1,11 +1,20 @@ +import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; - +import WorkItemParentInline from '~/work_items/components/work_item_parent_inline.vue'; +import WorkItemParent from '~/work_items/components/work_item_parent_with_edit.vue'; +import waitForPromises from 'helpers/wait_for_promises'; import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue'; -import { workItemResponseFactory } from '../mock_data'; +import { + workItemResponseFactory, + taskType, + issueType, + objectiveType, + keyResultType, +} from '../mock_data'; describe('WorkItemAttributesWrapper component', () => { let wrapper; @@ -16,8 +25,13 @@ describe('WorkItemAttributesWrapper component', () => { const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone); + const findWorkItemParentInline = () => wrapper.findComponent(WorkItemParentInline); + const findWorkItemParent = () => wrapper.findComponent(WorkItemParent); - const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => { + const createComponent = ({ + workItem = workItemQueryResponse.data.workItem, + workItemsMvc2 = true, + } = {}) => { wrapper = shallowMount(WorkItemAttributesWrapper, { propsData: { fullPath: 'group/project', @@ -29,6 +43,9 @@ describe('WorkItemAttributesWrapper component', () => { hasOkrsFeature: true, hasIssuableHealthStatusFeature: true, projectNamespace: 'namespace', + glFeatures: { + workItemsMvc2, + }, }, stubs: { WorkItemWeight: true, @@ -94,4 +111,54 @@ describe('WorkItemAttributesWrapper component', () => { expect(findWorkItemMilestone().exists()).toBe(exists); }); }); + + describe('parent widget', () => { + describe.each` + description | workItemType | exists + ${'when work item type is task'} | ${taskType} | ${true} + ${'when work item type is objective'} | ${objectiveType} | ${true} + ${'when work item type is keyresult'} | ${keyResultType} | ${true} + ${'when work item type is issue'} | ${issueType} | ${false} + `('$description', ({ workItemType, exists }) => { + it(`${exists ? 'renders' : 'does not render'} parent component`, async () => { + const response = workItemResponseFactory({ workItemType }); + createComponent({ workItem: response.data.workItem }); + + await waitForPromises(); + + expect(findWorkItemParent().exists()).toBe(exists); + }); + }); + + it('renders WorkItemParent when workItemsMvc2 enabled', async () => { + createComponent(); + + await waitForPromises(); + + expect(findWorkItemParent().exists()).toBe(true); + expect(findWorkItemParentInline().exists()).toBe(false); + }); + + it('renders WorkItemParentInline when workItemsMvc2 disabled', async () => { + createComponent({ workItemsMvc2: false }); + + await waitForPromises(); + + expect(findWorkItemParent().exists()).toBe(false); + expect(findWorkItemParentInline().exists()).toBe(true); + }); + + it('emits an error event to the wrapper', async () => { + const response = workItemResponseFactory({ parentWidgetPresent: true }); + createComponent({ workItem: response.data.workItem }); + const updateError = 'Failed to update'; + + await waitForPromises(); + + findWorkItemParent().vm.$emit('error', updateError); + await nextTick(); + + expect(wrapper.emitted('error')).toEqual([[updateError]]); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js index 6fa3a70c3eb..f77d6c89035 100644 --- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -61,7 +61,6 @@ describe('WorkItemDetailModal component', () => { expect(findWorkItemDetail().props()).toEqual({ isModal: true, workItemIid: '1', - workItemParentId: null, }); }); 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 acfe4571cd2..d63bb94c3f0 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -1,10 +1,4 @@ -import { - GlAlert, - GlSkeletonLoader, - GlButton, - GlEmptyState, - GlIntersectionObserver, -} from '@gitlab/ui'; +import { GlAlert, GlSkeletonLoader, GlEmptyState } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -15,6 +9,7 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { stubComponent } from 'helpers/stub_component'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; +import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue'; import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue'; @@ -23,13 +18,13 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; +import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue'; 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 { @@ -74,8 +69,7 @@ describe('WorkItemDetail component', () => { const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated); const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription); const findWorkItemAttributesWrapper = () => wrapper.findComponent(WorkItemAttributesWrapper); - const findParent = () => wrapper.findByTestId('work-item-parent'); - const findParentButton = () => findParent().findComponent(GlButton); + const findAncestors = () => wrapper.findComponent(WorkItemAncestors); const findCloseButton = () => wrapper.findByTestId('work-item-close'); const findWorkItemType = () => wrapper.findByTestId('work-item-type'); const findHierarchyTree = () => wrapper.findComponent(WorkItemTree); @@ -84,11 +78,9 @@ describe('WorkItemDetail component', () => { const findModal = () => wrapper.findComponent(WorkItemDetailModal); const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos); - const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); - const findStickyHeader = () => wrapper.findByTestId('work-item-sticky-header'); + const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader); const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview'); const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar'); - const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear'); const createComponent = ({ isGroup = false, @@ -96,7 +88,7 @@ describe('WorkItemDetail component', () => { updateInProgress = false, workItemIid = '1', handler = successHandler, - confidentialityMock = [updateWorkItemMutation, jest.fn()], + mutationHandler, error = undefined, workItemsMvc2Enabled = false, linkedWorkItemsEnabled = false, @@ -105,8 +97,8 @@ describe('WorkItemDetail component', () => { apolloProvider: createMockApollo([ [workItemByIidQuery, handler], [groupWorkItemByIidQuery, groupSuccessHandler], + [updateWorkItemMutation, mutationHandler], [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], - confidentialityMock, ]), isLoggedIn: isLoggedIn(), propsData: { @@ -134,6 +126,7 @@ describe('WorkItemDetail component', () => { reportAbusePath: '/report/abuse/path', }, stubs: { + WorkItemAncestors: true, WorkItemWeight: true, WorkItemIteration: true, WorkItemHealthStatus: true, @@ -236,119 +229,52 @@ describe('WorkItemDetail component', () => { describe('confidentiality', () => { const errorMessage = 'Mutation failed'; - const confidentialWorkItem = workItemByIidResponseFactory({ - confidential: true, - }); - const workItem = confidentialWorkItem.data.workspace.workItems.nodes[0]; - - // Mocks for work item without parent - const withoutParentExpectedInputVars = { id, confidential: true }; - const toggleConfidentialityWithoutParentHandler = jest.fn().mockResolvedValue({ - data: { - workItemUpdate: { - workItem, - errors: [], - }, - }, - }); - const withoutParentHandlerMock = jest - .fn() - .mockResolvedValue(workItemQueryResponseWithoutParent); - const confidentialityWithoutParentMock = [ - updateWorkItemMutation, - toggleConfidentialityWithoutParentHandler, - ]; - const confidentialityWithoutParentFailureMock = [ - updateWorkItemMutation, - jest.fn().mockRejectedValue(new Error(errorMessage)), - ]; - - // Mocks for work item with parent - const withParentExpectedInputVars = { - id: mockParent.parent.id, - taskData: { id, confidential: true }, - }; - const toggleConfidentialityWithParentHandler = jest.fn().mockResolvedValue({ + const confidentialWorkItem = workItemByIidResponseFactory({ confidential: true }); + const mutationHandler = jest.fn().mockResolvedValue({ data: { workItemUpdate: { - workItem: { - id: workItem.id, - descriptionHtml: workItem.description, - }, - task: { - workItem, - confidential: true, - }, + workItem: confidentialWorkItem.data.workspace.workItems.nodes[0], errors: [], }, }, }); - const confidentialityWithParentMock = [ - updateWorkItemTaskMutation, - toggleConfidentialityWithParentHandler, - ]; - const confidentialityWithParentFailureMock = [ - updateWorkItemTaskMutation, - jest.fn().mockRejectedValue(new Error(errorMessage)), - ]; - - describe.each` - context | handlerMock | confidentialityMock | confidentialityFailureMock | inputVariables - ${'no parent'} | ${withoutParentHandlerMock} | ${confidentialityWithoutParentMock} | ${confidentialityWithoutParentFailureMock} | ${withoutParentExpectedInputVars} - ${'parent'} | ${successHandler} | ${confidentialityWithParentMock} | ${confidentialityWithParentFailureMock} | ${withParentExpectedInputVars} - `( - 'when work item has $context', - ({ handlerMock, confidentialityMock, confidentialityFailureMock, inputVariables }) => { - it('sends updateInProgress props to child component', async () => { - createComponent({ - handler: handlerMock, - confidentialityMock, - }); - - await waitForPromises(); - - findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); - await nextTick(); - - expect(findCreatedUpdated().props('updateInProgress')).toBe(true); - }); + it('sends updateInProgress props to child component', async () => { + createComponent({ mutationHandler }); + await waitForPromises(); - it('emits workItemUpdated when mutation is successful', async () => { - createComponent({ - handler: handlerMock, - confidentialityMock, - }); + findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); + await nextTick(); - await waitForPromises(); + expect(findCreatedUpdated().props('updateInProgress')).toBe(true); + }); - findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); - await waitForPromises(); + it('emits workItemUpdated when mutation is successful', async () => { + createComponent({ mutationHandler }); + await waitForPromises(); - expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]); - expect(confidentialityMock[1]).toHaveBeenCalledWith({ - input: inputVariables, - }); - }); + findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); + await waitForPromises(); - it('shows an alert when mutation fails', async () => { - createComponent({ - handler: handlerMock, - confidentialityMock: confidentialityFailureMock, - }); + expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]); + expect(mutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + confidential: true, + }, + }); + }); - await waitForPromises(); - findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); - await waitForPromises(); - expect(wrapper.emitted('workItemUpdated')).toBeUndefined(); + it('shows an alert when mutation fails', async () => { + createComponent({ mutationHandler: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + await waitForPromises(); - await nextTick(); + findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); + await waitForPromises(); - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(errorMessage); - }); - }, - ); + expect(wrapper.emitted('workItemUpdated')).toBeUndefined(); + expect(findAlert().text()).toBe(errorMessage); + }); }); describe('description', () => { @@ -366,19 +292,19 @@ describe('WorkItemDetail component', () => { }); }); - describe('secondary breadcrumbs', () => { - it('does not show secondary breadcrumbs by default', () => { + describe('ancestors widget', () => { + it('does not show ancestors widget by default', () => { createComponent(); - expect(findParent().exists()).toBe(false); + expect(findAncestors().exists()).toBe(false); }); - it('does not show secondary breadcrumbs if there is not a parent', async () => { + it('does not show ancestors widget if there is not a parent', async () => { createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) }); await waitForPromises(); - expect(findParent().exists()).toBe(false); + expect(findAncestors().exists()).toBe(false); }); it('shows title in the header when there is no parent', async () => { @@ -396,45 +322,8 @@ describe('WorkItemDetail component', () => { return waitForPromises(); }); - it('shows secondary breadcrumbs if there is a parent', () => { - expect(findParent().exists()).toBe(true); - }); - - it('shows parent breadcrumb icon', () => { - expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName); - }); - - it('shows parent title and iid', () => { - expect(findParentButton().text()).toBe( - `${mockParent.parent.title} #${mockParent.parent.iid}`, - ); - }); - - it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => { - expect(findParentButton().attributes().href).toBe('../../-/issues/5'); - }); - - it('sets the parent breadcrumb URL based on parent webUrl when parent type is not `Issue`', async () => { - const mockParentObjective = { - parent: { - ...mockParent.parent, - workItemType: { - id: mockParent.parent.workItemType.id, - name: 'Objective', - iconName: 'issue-type-objective', - }, - }, - }; - const parentResponse = workItemByIidResponseFactory(mockParentObjective); - createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) }); - await waitForPromises(); - - expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl); - }); - - it('shows work item type and iid', () => { - const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0]; - expect(findParent().text()).toContain(`#${iid}`); + it('shows ancestors widget if there is a parent', () => { + expect(findAncestors().exists()).toBe(true); }); it('does not show title in the header when parent exists', () => { @@ -769,8 +658,7 @@ describe('WorkItemDetail component', () => { expect(findWorkItemTwoColumnViewContainer().classes()).not.toContain('work-item-overview'); }); - it('does not have sticky header', () => { - expect(findIntersectionObserver().exists()).toBe(false); + it('does not have sticky header component', () => { expect(findStickyHeader().exists()).toBe(false); }); @@ -789,18 +677,7 @@ describe('WorkItemDetail component', () => { expect(findWorkItemTwoColumnViewContainer().classes()).toContain('work-item-overview'); }); - it('does not show sticky header by default', () => { - expect(findStickyHeader().exists()).toBe(false); - }); - - it('has the sticky header when the page is scrolled', async () => { - expect(findIntersectionObserver().exists()).toBe(true); - - global.pageYOffset = 100; - triggerPageScroll(); - - await nextTick(); - + it('renders the work item sticky header component', () => { expect(findStickyHeader().exists()).toBe(true); }); diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_actions_split_button_spec.js index 55d5b34ae70..630ffa1a699 100644 --- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_actions_split_button_spec.js @@ -1,12 +1,40 @@ import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue'; +import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +const okrActions = [ + { + name: 'Objective', + items: [ + { + text: 'New objective', + }, + { + text: 'Existing objective', + }, + ], + }, + { + name: 'Key result', + items: [ + { + text: 'New key result', + }, + { + text: 'Existing key result', + }, + ], + }, +]; + const createComponent = () => { return extendedWrapper( - shallowMount(OkrActionsSplitButton, { + shallowMount(WorkItemActionsSplitButton, { + propsData: { + actions: okrActions, + }, stubs: { GlDisclosureDropdown, }, @@ -21,7 +49,7 @@ describe('RelatedItemsTree', () => { wrapper = createComponent(); }); - describe('OkrActionsSplitButton', () => { + describe('WorkItemActionsSplitButton', () => { describe('template', () => { it('renders objective and key results sections', () => { expect(wrapper.findAllComponents(GlDisclosureDropdownGroup).at(0).props('group').name).toBe( 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 6c1d1035c3d..49a674e73c8 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 @@ -1,28 +1,36 @@ -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { GlToggle } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; -import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue'; +import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue'; +import getAllowedWorkItemChildTypes from '~/work_items//graphql/work_item_allowed_children.query.graphql'; import { FORM_TYPES, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, } from '~/work_items/constants'; -import { childrenWorkItems } from '../../mock_data'; +import { childrenWorkItems, allowedChildrenTypesResponse } from '../../mock_data'; + +Vue.use(VueApollo); describe('WorkItemTree', () => { let wrapper; const findEmptyState = () => wrapper.findByTestId('tree-empty'); - const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton); + const findToggleFormSplitButton = () => wrapper.findComponent(WorkItemActionsSplitButton); const findForm = () => wrapper.findComponent(WorkItemLinksForm); const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper); const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper); const findShowLabelsToggle = () => wrapper.findComponent(GlToggle); + const allowedChildrenTypesHandler = jest.fn().mockResolvedValue(allowedChildrenTypesResponse); + const createComponent = ({ workItemType = 'Objective', parentWorkItemType = 'Objective', @@ -31,6 +39,9 @@ describe('WorkItemTree', () => { canUpdate = true, } = {}) => { wrapper = shallowMountExtended(WorkItemTree, { + apolloProvider: createMockApollo([ + [getAllowedWorkItemChildTypes, allowedChildrenTypesHandler], + ]), propsData: { fullPath: 'test/project', workItemType, @@ -79,18 +90,25 @@ describe('WorkItemTree', () => { expect(findWidgetWrapper().props('error')).toBe(errorMessage); }); + it('fetches allowed children types for current work item', async () => { + createComponent(); + await waitForPromises(); + + expect(allowedChildrenTypesHandler).toHaveBeenCalled(); + }); + it.each` - option | event | formType | childType - ${'New objective'} | ${'showCreateObjectiveForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE} - ${'Existing objective'} | ${'showAddObjectiveForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE} - ${'New key result'} | ${'showCreateKeyResultForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} - ${'Existing key result'} | ${'showAddKeyResultForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} + option | formType | childType + ${'New objective'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE} + ${'Existing objective'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE} + ${'New key result'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} + ${'Existing key result'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} `( - 'when selecting $option from split button, renders the form passing $formType and $childType', - async ({ event, formType, childType }) => { + 'when triggering action $option, renders the form passing $formType and $childType', + async ({ formType, childType }) => { createComponent(); - findToggleFormSplitButton().vm.$emit(event); + wrapper.vm.showAddForm(formType, childType); await nextTick(); expect(findForm().exists()).toBe(true); @@ -122,7 +140,7 @@ describe('WorkItemTree', () => { it('emits `addChild` event when form emits `addChild` event', async () => { createComponent(); - findToggleFormSplitButton().vm.$emit('showCreateObjectiveForm'); + wrapper.vm.showAddForm(FORM_TYPES.create, WORK_ITEM_TYPE_ENUM_OBJECTIVE); await nextTick(); findForm().vm.$emit('addChild'); 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 9e02e0708d4..2620242000e 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -10,6 +10,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue'; import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; +import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql'; import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql'; @@ -63,6 +64,9 @@ describe('WorkItemNotes component', () => { const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index); const findDeleteNoteModal = () => wrapper.findComponent(GlModal); + const groupWorkItemNotesQueryHandler = jest + .fn() + .mockResolvedValue(mockWorkItemNotesByIidResponse); const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesByIidResponse); const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse); const workItemNotesWithCommentsQueryHandler = jest @@ -87,17 +91,22 @@ describe('WorkItemNotes component', () => { workItemIid = mockWorkItemIid, defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler, deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler, + isGroup = false, isModal = false, isWorkItemConfidential = false, } = {}) => { wrapper = shallowMount(WorkItemNotes, { apolloProvider: createMockApollo([ [workItemNotesByIidQuery, defaultWorkItemNotesQueryHandler], + [groupWorkItemNotesByIidQuery, groupWorkItemNotesQueryHandler], [deleteWorkItemNoteMutation, deleteWINoteMutationHandler], [workItemNoteCreatedSubscription, notesCreateSubscriptionHandler], [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler], [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler], ]), + provide: { + isGroup, + }, propsData: { fullPath: 'test-path', workItemId, @@ -354,4 +363,22 @@ describe('WorkItemNotes component', () => { expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true); }); + + describe('when project context', () => { + it('calls the project work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(workItemNotesQueryHandler).toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('calls the group work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(groupWorkItemNotesQueryHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_inline_spec.js index 11fe6dffbfa..3e4f99d5935 100644 --- a/spec/frontend/work_items/components/work_item_parent_spec.js +++ b/spec/frontend/work_items/components/work_item_parent_inline_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; -import WorkItemParent from '~/work_items/components/work_item_parent.vue'; +import WorkItemParentInline from '~/work_items/components/work_item_parent_inline.vue'; import { removeHierarchyChild } from '~/work_items/graphql/cache_utils'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql'; @@ -26,7 +26,7 @@ jest.mock('~/work_items/graphql/cache_utils', () => ({ removeHierarchyChild: jest.fn(), })); -describe('WorkItemParent component', () => { +describe('WorkItemParentInline component', () => { Vue.use(VueApollo); let wrapper; @@ -50,7 +50,7 @@ describe('WorkItemParent component', () => { mutationHandler = successUpdateWorkItemMutationHandler, isGroup = false, } = {}) => { - wrapper = shallowMountExtended(WorkItemParent, { + wrapper = shallowMountExtended(WorkItemParentInline, { apolloProvider: createMockApollo([ [projectWorkItemsQuery, searchQueryHandler], [groupWorkItemsQuery, groupWorkItemsSuccessHandler], diff --git a/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js b/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js new file mode 100644 index 00000000000..61e43456479 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js @@ -0,0 +1,409 @@ +import { GlForm, GlCollapsibleListbox } 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 { mountExtended } from 'helpers/vue_test_utils_helper'; +import { __ } from '~/locale'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import WorkItemParent from '~/work_items/components/work_item_parent_with_edit.vue'; +import { removeHierarchyChild } from '~/work_items/graphql/cache_utils'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.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/sentry_browser_wrapper'); +jest.mock('~/work_items/graphql/cache_utils', () => ({ + removeHierarchyChild: jest.fn(), +})); + +describe('WorkItemParent component', () => { + Vue.use(VueApollo); + + let wrapper; + + const workItemId = 'gid://gitlab/WorkItem/1'; + const workItemType = 'Objective'; + const mockFullPath = 'full-path'; + + const groupWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse); + const availableWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse); + const availableWorkItemsFailureHandler = jest.fn().mockRejectedValue(new Error()); + + const findHeader = () => wrapper.find('h3'); + const findEditButton = () => wrapper.find('[data-testid="edit-parent"]'); + const findApplyButton = () => wrapper.find('[data-testid="apply-parent"]'); + + const findLoadingIcon = () => wrapper.find('[data-testid="loading-icon-parent"]'); + const findLabel = () => wrapper.find('label'); + const findForm = () => wrapper.findComponent(GlForm); + const findCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); + + const successUpdateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: mockParentWidgetResponse })); + + const createComponent = ({ + canUpdate = true, + parent = null, + searchQueryHandler = availableWorkItemsSuccessHandler, + mutationHandler = successUpdateWorkItemMutationHandler, + isEditing = false, + isGroup = false, + } = {}) => { + wrapper = mountExtended(WorkItemParent, { + apolloProvider: createMockApollo([ + [projectWorkItemsQuery, searchQueryHandler], + [groupWorkItemsQuery, groupWorkItemsSuccessHandler], + [updateWorkItemMutation, mutationHandler], + ]), + provide: { + fullPath: mockFullPath, + isGroup, + }, + propsData: { + canUpdate, + parent, + workItemId, + workItemType, + }, + }); + + if (isEditing) { + findEditButton().trigger('click'); + } + }; + + beforeEach(() => { + createComponent(); + }); + + describe('label', () => { + it('shows header when not editing', () => { + createComponent(); + + expect(findHeader().exists()).toBe(true); + expect(findHeader().classes('gl-sr-only')).toBe(false); + expect(findLabel().exists()).toBe(false); + }); + + it('shows label and hides header while editing', async () => { + createComponent({ isEditing: true }); + + await nextTick(); + + expect(findLabel().exists()).toBe(true); + expect(findHeader().classes('gl-sr-only')).toBe(true); + }); + }); + + describe('edit button', () => { + it('is not shown if user cannot edit', () => { + createComponent({ canUpdate: false }); + + expect(findEditButton().exists()).toBe(false); + }); + + it('is shown if user can edit', () => { + createComponent({ canUpdate: true }); + + expect(findEditButton().exists()).toBe(true); + }); + + it('triggers edit mode on click', async () => { + createComponent(); + + findEditButton().trigger('click'); + + await nextTick(); + + expect(findLabel().exists()).toBe(true); + expect(findForm().exists()).toBe(true); + }); + + it('is replaced by Apply button while editing', async () => { + createComponent(); + + findEditButton().trigger('click'); + + await nextTick(); + + expect(findEditButton().exists()).toBe(false); + expect(findApplyButton().exists()).toBe(true); + }); + }); + + describe('loading icon', () => { + const selectWorkItem = async (workItem) => { + await findCollapsibleListbox().vm.$emit('select', workItem); + }; + + it('shows loading icon while update is in progress', async () => { + createComponent(); + findEditButton().trigger('click'); + + await nextTick(); + + selectWorkItem('gid://gitlab/WorkItem/716'); + + await nextTick(); + expect(findLoadingIcon().exists()).toBe(true); + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + it('shows loading icon when unassign is clicked', async () => { + createComponent({ parent: mockParentWidgetResponse }); + findEditButton().trigger('click'); + + await nextTick(); + + findCollapsibleListbox().vm.$emit('reset'); + + await nextTick(); + expect(findLoadingIcon().exists()).toBe(true); + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + }); + + describe('value', () => { + it('shows None when no parent is set', () => { + createComponent(); + + expect(wrapper.text()).toContain(__('None')); + }); + + it('shows parent when parent is set', () => { + createComponent({ parent: mockParentWidgetResponse }); + + expect(wrapper.text()).not.toContain(__('None')); + expect(wrapper.text()).toContain(mockParentWidgetResponse.title); + }); + }); + + describe('form', () => { + it('is not shown while not editing', async () => { + await createComponent(); + + expect(findForm().exists()).toBe(false); + }); + + it('is shown while editing', async () => { + await createComponent({ isEditing: true }); + + expect(findForm().exists()).toBe(true); + }); + }); + + describe('Parent Input', () => { + it('is not shown while not editing', async () => { + await createComponent(); + + expect(findCollapsibleListbox().exists()).toBe(false); + }); + + it('renders the collapsible listbox with required props', async () => { + await createComponent({ isEditing: true }); + + expect(findCollapsibleListbox().exists()).toBe(true); + expect(findCollapsibleListbox().props()).toMatchObject({ + items: [], + headerText: 'Assign parent', + category: 'primary', + loading: false, + isCheckCentered: true, + searchable: true, + searching: false, + infiniteScroll: false, + noResultsText: 'No matching results', + toggleText: 'None', + searchPlaceholder: 'Search', + resetButtonLabel: 'Unassign', + }); + }); + it('shows loading while searching', async () => { + await createComponent({ isEditing: true }); + + await findCollapsibleListbox().vm.$emit('shown'); + expect(findCollapsibleListbox().props('searching')).toBe(true); + }); + }); + + describe('work items query', () => { + it('loads work items in the listbox', async () => { + await createComponent({ isEditing: true }); + 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 () => { + await createComponent({ + searchQueryHandler: availableWorkItemsFailureHandler, + isEditing: true, + }); + + 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); + await createComponent({ + searchQueryHandler: searchedItemQueryHandler, + isEditing: true, + }); + + await findCollapsibleListbox().vm.$emit('shown'); + + await waitForPromises(); + + expect(searchedItemQueryHandler).toHaveBeenCalledWith({ + fullPath: 'full-path', + searchTerm: '', + types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], + in: undefined, + iid: null, + isNumber: false, + }); + + await findCollapsibleListbox().vm.$emit('search', 'Objective 101'); + + expect(searchedItemQueryHandler).toHaveBeenCalledWith({ + fullPath: 'full-path', + searchTerm: 'Objective 101', + types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE], + in: 'TITLE', + iid: null, + isNumber: false, + }); + + 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('select', workItem); + }; + + it('calls mutation when item is selected', async () => { + await createComponent({ isEditing: true }); + selectWorkItem('gid://gitlab/WorkItem/716'); + + await waitForPromises(); + + expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/716', + }, + }, + }); + + expect(removeHierarchyChild).toHaveBeenCalledWith({ + cache: expect.anything(Object), + fullPath: mockFullPath, + iid: undefined, + isGroup: false, + workItem: { id: 'gid://gitlab/WorkItem/1' }, + }); + }); + + it('calls mutation when item is unassigned', async () => { + const unAssignParentWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: null })); + await createComponent({ + parent: { + iid: '1', + }, + mutationHandler: unAssignParentWorkItemMutationHandler, + }); + + findEditButton().trigger('click'); + + await nextTick(); + + findCollapsibleListbox().vm.$emit('reset'); + + await waitForPromises(); + + expect(unAssignParentWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + hierarchyWidget: { + parentId: null, + }, + }, + }); + expect(removeHierarchyChild).toHaveBeenCalledWith({ + cache: expect.anything(Object), + fullPath: mockFullPath, + iid: '1', + isGroup: false, + workItem: { id: 'gid://gitlab/WorkItem/1' }, + }); + }); + + it('emits error when mutation fails', async () => { + await createComponent({ + mutationHandler: jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse), + isEditing: true, + }); + + 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'); + await createComponent({ + mutationHandler: jest.fn().mockRejectedValue(error), + isEditing: true, + }); + + 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_state_toggle_button_spec.js b/spec/frontend/work_items/components/work_item_state_toggle_spec.js index a210bd50422..a210bd50422 100644 --- a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js +++ b/spec/frontend/work_items/components/work_item_state_toggle_spec.js diff --git a/spec/frontend/work_items/components/work_item_sticky_header_spec.js b/spec/frontend/work_items/components/work_item_sticky_header_spec.js new file mode 100644 index 00000000000..4b7818044b1 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_sticky_header_spec.js @@ -0,0 +1,59 @@ +import { GlIntersectionObserver } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { STATE_OPEN } from '~/work_items/constants'; +import { workItemResponseFactory } from 'jest/work_items/mock_data'; +import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue'; +import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; +import WorkItemActions from '~/work_items/components/work_item_actions.vue'; +import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; + +describe('WorkItemStickyHeader', () => { + let wrapper; + + const workItemResponse = workItemResponseFactory({ canUpdate: true, confidential: true }).data + .workItem; + + const createComponent = () => { + wrapper = shallowMountExtended(WorkItemStickyHeader, { + propsData: { + workItem: workItemResponse, + fullPath: '/test', + isStickyHeaderShowing: true, + workItemNotificationsSubscribed: true, + updateInProgress: false, + parentWorkItemConfidentiality: false, + showWorkItemCurrentUserTodos: true, + isModal: false, + currentUserTodos: [], + workItemState: STATE_OPEN, + }, + }); + }; + const findStickyHeader = () => wrapper.findByTestId('work-item-sticky-header'); + const findConfidentialityBadge = () => wrapper.findComponent(ConfidentialityBadge); + const findWorkItemActions = () => wrapper.findComponent(WorkItemActions); + const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos); + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear'); + + beforeEach(() => { + createComponent(); + }); + + it('has the sticky header when the page is scrolled', async () => { + global.pageYOffset = 100; + triggerPageScroll(); + + await nextTick(); + + expect(findStickyHeader().exists()).toBe(true); + }); + + it('has the components of confidentiality, actions, todos and title', () => { + expect(findConfidentialityBadge().exists()).toBe(true); + expect(findWorkItemActions().exists()).toBe(true); + expect(findWorkItemTodos().exists()).toBe(true); + expect(wrapper.findByText(workItemResponse.title).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index 0f466bcf691..de740e5fbc5 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -8,7 +8,6 @@ import ItemTitle from '~/work_items/components/item_title.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; -import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data'; describe('WorkItemTitle component', () => { @@ -20,22 +19,14 @@ describe('WorkItemTitle component', () => { const findItemTitle = () => wrapper.findComponent(ItemTitle); - const createComponent = ({ - workItemParentId, - mutationHandler = mutationSuccessHandler, - canUpdate = true, - } = {}) => { + const createComponent = ({ mutationHandler = mutationSuccessHandler, canUpdate = true } = {}) => { const { id, title, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemTitle, { - apolloProvider: createMockApollo([ - [updateWorkItemMutation, mutationHandler], - [updateWorkItemTaskMutation, mutationHandler], - ]), + apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), propsData: { workItemId: id, workItemTitle: title, workItemType: workItemType.name, - workItemParentId, canUpdate, }, }); @@ -77,27 +68,6 @@ describe('WorkItemTitle component', () => { }); }); - it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => { - const title = 'new title!'; - const workItemParentId = '1234'; - - createComponent({ - workItemParentId, - }); - - findItemTitle().vm.$emit('title-changed', title); - - expect(mutationSuccessHandler).toHaveBeenCalledWith({ - input: { - id: workItemParentId, - taskData: { - id: workItemQueryResponse.data.workItem.id, - title, - }, - }, - }); - }); - it('does not call a mutation when the title has not changed', () => { createComponent(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 8df46403b90..9d4606eb95a 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -445,7 +445,7 @@ export const descriptionHtmlWithCheckboxes = ` </ul> `; -const taskType = { +export const taskType = { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', name: 'Task', @@ -459,6 +459,20 @@ export const objectiveType = { iconName: 'issue-type-objective', }; +export const keyResultType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Key Result', + iconName: 'issue-type-keyresult', +}; + +export const issueType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Issue', + iconName: 'issue-type-issue', +}; + export const mockEmptyLinkedItems = { type: WIDGET_TYPE_LINKED_ITEMS, blocked: false, @@ -3703,5 +3717,40 @@ export const updateWorkItemNotificationsMutationResponse = (subscribed) => ({ }, }); +export const allowedChildrenTypesResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/634', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + widgetDefinitions: [ + { + type: 'HIERARCHY', + allowedChildTypes: { + nodes: [ + { + id: 'gid://gitlab/WorkItems::Type/7', + name: 'Key Result', + __typename: 'WorkItemType', + }, + { + id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + __typename: 'WorkItemType', + }, + ], + __typename: 'WorkItemTypeConnection', + }, + __typename: 'WorkItemWidgetDefinitionHierarchy', + }, + ], + __typename: 'WorkItemType', + }, + __typename: 'WorkItem', + }, + }, +}; + export const generateWorkItemsListWithId = (count) => Array.from({ length: count }, (_, i) => ({ id: `gid://gitlab/WorkItem/${i + 1}` })); diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js index 8ae32ce5f40..43eceb13b67 100644 --- a/spec/frontend/work_items/notes/award_utils_spec.js +++ b/spec/frontend/work_items/notes/award_utils_spec.js @@ -2,6 +2,7 @@ import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_uti import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import mockApollo from 'helpers/mock_apollo_helper'; import { __ } from '~/locale'; +import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; @@ -105,5 +106,22 @@ describe('Work item note award utils', () => { expect(updatedNote.awardEmoji.nodes).toEqual([]); }); + + it.each` + description | isGroup | query + ${'calls project query when in project context'} | ${false} | ${workItemNotesByIidQuery} + ${'calls group query when in group context'} | ${true} | ${groupWorkItemNotesByIidQuery} + `('$description', ({ isGroup, query }) => { + const note = firstNote; + const { name } = mockAwardEmojiThumbsUp; + const cacheSpy = { updateQuery: jest.fn() }; + + optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid })(cacheSpy); + + expect(cacheSpy.updateQuery).toHaveBeenCalledWith( + { query, variables: { fullPath, iid: workItemIid } }, + expect.any(Function), + ); + }); }); }); 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 527f5890338..2c898f97ee9 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -8,7 +8,6 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import ItemTitle from '~/work_items/components/item_title.vue'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; -import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql'; import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data'; jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] })); @@ -42,7 +41,6 @@ describe('Create work item component', () => { [ [projectWorkItemTypesQuery, queryHandler], [createWorkItemMutation, mutationHandler], - [createWorkItemFromTaskMutation, mutationHandler], ], {}, { typePolicies: { Project: { merge: true } } }, diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js index 84b10f30418..4854b5bfb77 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -49,7 +49,6 @@ describe('Work items root component', () => { expect(findWorkItemDetail().props()).toEqual({ isModal: false, - workItemParentId: null, workItemIid: '1', }); }); |