diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 17:22:11 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 17:22:11 +0300 |
commit | 0c872e02b2c822e3397515ec324051ff540f0cd5 (patch) | |
tree | ce2fb6ce7030e4dad0f4118d21ab6453e5938cdd /spec/frontend/work_items | |
parent | f7e05a6853b12f02911494c4b3fe53d9540d74fc (diff) |
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'spec/frontend/work_items')
19 files changed, 1507 insertions, 180 deletions
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js new file mode 100644 index 00000000000..3e3b8bf65b2 --- /dev/null +++ b/spec/frontend/work_items/components/notes/system_note_spec.js @@ -0,0 +1,111 @@ +import { GlIcon } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import { renderGFM } from '~/behaviors/markdown/render_gfm'; +import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue'; +import NoteHeader from '~/notes/components/note_header.vue'; +import axios from '~/lib/utils/axios_utils'; + +jest.mock('~/behaviors/markdown/render_gfm'); + +describe('system note component', () => { + let wrapper; + let props; + let mock; + + const findTimelineIcon = () => wrapper.findComponent(GlIcon); + const findSystemNoteMessage = () => wrapper.findComponent(NoteHeader); + const findOutdatedLineButton = () => + wrapper.findComponent('[data-testid="outdated-lines-change-btn"]'); + const findOutdatedLines = () => wrapper.findComponent('[data-testid="outdated-lines"]'); + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(WorkItemSystemNote, { + propsData, + slots: { + 'extra-controls': + '<gl-button data-testid="outdated-lines-change-btn">Compare with last version</gl-button>', + }, + }); + }; + + beforeEach(() => { + props = { + note: { + id: '1424', + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatarUrl: 'path', + path: '/root', + }, + bodyHtml: '<p dir="auto">closed</p>', + systemNoteIconName: 'status_closed', + createdAt: '2017-08-02T10:51:58.559Z', + }, + }; + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('should render a list item with correct id', () => { + createComponent(props); + + expect(wrapper.attributes('id')).toBe(`note_${props.note.id}`); + }); + + // Note: The test case below is to handle a use case related to vuex store but since this does not + // have a vuex store , disabling it now will be fixing it in the next iteration + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should render target class is note is target note', () => { + createComponent(props); + + expect(wrapper.classes()).toContain('target'); + }); + + it('should render svg icon', () => { + createComponent(props); + + expect(findTimelineIcon().exists()).toBe(true); + }); + + // Redcarpet Markdown renderer wraps text in `<p>` tags + // we need to strip them because they break layout of commit lists in system notes: + // https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png + it('removes wrapping paragraph from note HTML', () => { + createComponent(props); + + expect(findSystemNoteMessage().html()).toContain('<span>closed</span>'); + }); + + it('should renderGFM onMount', () => { + createComponent(props); + + expect(renderGFM).toHaveBeenCalled(); + }); + + // eslint-disable-next-line jest/no-disabled-tests + it.skip('renders outdated code lines', async () => { + mock + .onGet('/outdated_line_change_path') + .reply(200, [ + { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 }, + ]); + + createComponent({ + note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' }, + }); + + await findOutdatedLineButton().vm.$emit('click'); + await waitForPromises(); + + expect(findOutdatedLines().exists()).toBe(true); + }); +}); 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 7367212e49f..e85f62b881d 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -435,6 +435,20 @@ describe('WorkItemAssignees component', () => { expect(findTokenSelector().props('containerClass')).toBe('gl-shadow-none!'); }); + + it('calls the mutation for updating assignees with the correct input', async () => { + findTokenSelector().vm.$emit('input', [mockAssignees[1]]); + await waitForPromises(); + + expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + assigneesWidget: { + assigneeIds: [mockAssignees[1].id], + }, + id: 'gid://gitlab/WorkItem/1', + }, + }); + }); }); describe('tracking', () => { diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js index 01ab7824975..0ab2546440b 100644 --- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js +++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js @@ -1,9 +1,11 @@ import { shallowMount } from '@vue/test-utils'; -import $ from 'jquery'; import { nextTick } from 'vue'; import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue'; +import { renderGFM } from '~/behaviors/markdown/render_gfm'; import { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data'; +jest.mock('~/behaviors/markdown/render_gfm'); + describe('WorkItemDescription', () => { let wrapper; @@ -32,13 +34,11 @@ describe('WorkItemDescription', () => { }); it('renders gfm', async () => { - const renderGFMSpy = jest.spyOn($.fn, 'renderGFM'); - createComponent(); await nextTick(); - expect(renderGFMSpy).toHaveBeenCalled(); + expect(renderGFM).toHaveBeenCalled(); }); describe('with checkboxes', () => { 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 c79b049442d..05476ef5ca0 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -38,7 +38,7 @@ describe('WorkItemDescription', () => { const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse); const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); let workItemResponseHandler; - let workItemsMvc2; + let workItemsMvc; const findMarkdownField = () => wrapper.findComponent(MarkdownField); const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); @@ -46,7 +46,7 @@ describe('WorkItemDescription', () => { const findEditedAt = () => wrapper.findComponent(EditedAt); const editDescription = (newText) => { - if (workItemsMvc2) { + if (workItemsMvc) { return findMarkdownEditor().vm.$emit('input', newText); } return wrapper.find('textarea').setValue(newText); @@ -60,6 +60,7 @@ describe('WorkItemDescription', () => { canUpdate = true, workItemResponse = workItemResponseFactory({ canUpdate }), isEditing = false, + queryVariables = { id: workItemId }, fetchByIid = false, } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); @@ -75,14 +76,12 @@ describe('WorkItemDescription', () => { propsData: { workItemId: id, fullPath: 'test-project-path', - queryVariables: { - id: workItemId, - }, + queryVariables, fetchByIid, }, provide: { glFeatures: { - workItemsMvc2, + workItemsMvc, }, }, stubs: { @@ -104,11 +103,21 @@ describe('WorkItemDescription', () => { }); describe.each([true, false])( - 'editing description with workItemsMvc2 %workItemsMvc2Enabled', - (workItemsMvc2Enabled) => { + 'editing description with workItemsMvc %workItemsMvcEnabled', + (workItemsMvcEnabled) => { beforeEach(() => { beforeEach(() => { - workItemsMvc2 = workItemsMvc2Enabled; + workItemsMvc = workItemsMvcEnabled; + }); + }); + + it('has a subscription', async () => { + createComponent(); + + await waitForPromises(); + + expect(subscriptionHandler).toHaveBeenCalledWith({ + issuableId: workItemQueryResponse.data.workItem.id, }); }); @@ -275,6 +284,13 @@ describe('WorkItemDescription', () => { expect(workItemResponseHandler).not.toHaveBeenCalled(); expect(workItemByIidResponseHandler).toHaveBeenCalled(); }); + + it('skips calling the handlers when missing the needed queryVariables', async () => { + createComponent({ queryVariables: {}, fetchByIid: false }); + await waitForPromises(); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); }, ); }); 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 4029e47c390..686641800b3 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 @@ -86,7 +86,7 @@ describe('WorkItemDetailModal component', () => { isModal: true, workItemId: defaultPropsData.workItemId, workItemParentId: defaultPropsData.issueGid, - iid: null, + workItemIid: 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 26777b57797..bbab45c7055 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -11,7 +11,7 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import setWindowLocation from 'helpers/set_window_location_helper'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; @@ -21,7 +21,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; -import WorkItemInformation from '~/work_items/components/work_item_information.vue'; +import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; @@ -31,7 +31,6 @@ import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assign import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.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 { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { mockParent, workItemDatesSubscriptionResponse, @@ -40,11 +39,11 @@ import { workItemAssigneesSubscriptionResponse, workItemMilestoneSubscriptionResponse, projectWorkItemResponse, + objectiveType, } from '../mock_data'; describe('WorkItemDetail component', () => { let wrapper; - useLocalStorageSpy(); Vue.use(VueApollo); @@ -81,8 +80,7 @@ describe('WorkItemDetail component', () => { const findParentButton = () => findParent().findComponent(GlButton); const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]'); - const findWorkItemInformationAlert = () => wrapper.findComponent(WorkItemInformation); - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findHierarchyTree = () => wrapper.findComponent(WorkItemTree); const createComponent = ({ isModal = false, @@ -92,9 +90,9 @@ describe('WorkItemDetail component', () => { subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], error = undefined, + workItemsMvcEnabled = false, workItemsMvc2Enabled = false, fetchByIid = false, - iidPathQueryParam = undefined, } = {}) => { const handlers = [ [workItemQuery, handler], @@ -108,7 +106,7 @@ describe('WorkItemDetail component', () => { wrapper = shallowMount(WorkItemDetail, { apolloProvider: createMockApollo(handlers), - propsData: { isModal, workItemId, iid: '1' }, + propsData: { isModal, workItemId, workItemIid: '1' }, data() { return { updateInProgress, @@ -117,11 +115,14 @@ describe('WorkItemDetail component', () => { }, provide: { glFeatures: { + workItemsMvc: workItemsMvcEnabled, workItemsMvc2: workItemsMvc2Enabled, useIidInWorkItemsPath: fetchByIid, }, hasIssueWeightsFeature: true, hasIterationsFeature: true, + hasOkrsFeature: true, + hasIssuableHealthStatusFeature: true, projectNamespace: 'namespace', fullPath: 'group/project', }, @@ -129,18 +130,12 @@ describe('WorkItemDetail component', () => { WorkItemWeight: true, WorkItemIteration: true, }, - mocks: { - $route: { - query: { - iid_path: iidPathQueryParam, - }, - }, - }, }); }; afterEach(() => { wrapper.destroy(); + setWindowLocation(''); }); describe('when there is no `workItemId` prop', () => { @@ -406,9 +401,31 @@ describe('WorkItemDetail component', () => { expect(findWorkItemType().exists()).toBe(false); }); - it('sets the parent breadcrumb URL', () => { + it('shows parent breadcrumb icon', () => { + expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName); + }); + + 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 = workItemResponseFactory(mockParentObjective); + createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) }); + await waitForPromises(); + + expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl); + }); }); }); @@ -563,7 +580,7 @@ describe('WorkItemDetail component', () => { `('$description', async ({ milestoneWidgetPresent, exists }) => { const response = workItemResponseFactory({ milestoneWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler, workItemsMvc2Enabled: true }); + createComponent({ handler }); await waitForPromises(); expect(findWorkItemMilestone().exists()).toBe(exists); @@ -594,24 +611,6 @@ describe('WorkItemDetail component', () => { }); }); - describe('work item information', () => { - beforeEach(() => { - createComponent(); - return waitForPromises(); - }); - - it('is visible when viewed for the first time and sets localStorage value', async () => { - localStorage.clear(); - expect(findWorkItemInformationAlert().exists()).toBe(true); - expect(findLocalStorageSync().props('value')).toBe(true); - }); - - it('is not visible after reading local storage input', async () => { - await findLocalStorageSync().vm.$emit('input', false); - expect(findWorkItemInformationAlert().exists()).toBe(false); - }); - }); - it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => { createComponent(); await waitForPromises(); @@ -633,6 +632,8 @@ describe('WorkItemDetail component', () => { }); it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => { + setWindowLocation(`?iid_path=true`); + createComponent({ fetchByIid: true, iidPathQueryParam: 'true' }); await waitForPromises(); @@ -642,4 +643,24 @@ describe('WorkItemDetail component', () => { iid: '1', }); }); + + describe('hierarchy widget', () => { + it('does not render children tree by default', async () => { + createComponent(); + await waitForPromises(); + + expect(findHierarchyTree().exists()).toBe(false); + }); + + it('renders children tree when work item is an Objective', async () => { + const objectiveWorkItem = workItemResponseFactory({ + workItemType: objectiveType, + }); + const handler = jest.fn().mockResolvedValue(objectiveWorkItem); + createComponent({ handler }); + await waitForPromises(); + + expect(findHierarchyTree().exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js deleted file mode 100644 index 887c5f615e9..00000000000 --- a/spec/frontend/work_items/components/work_item_information_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlAlert, GlLink } from '@gitlab/ui'; -import WorkItemInformation from '~/work_items/components/work_item_information.vue'; -import { helpPagePath } from '~/helpers/help_page_helper'; - -const createComponent = () => mount(WorkItemInformation); - -describe('Work item information alert', () => { - let wrapper; - const tasksHelpPath = helpPagePath('user/tasks'); - - const findAlert = () => wrapper.findComponent(GlAlert); - const findHelpLink = () => wrapper.findComponent(GlLink); - beforeEach(() => { - wrapper = createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('should be visible', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('should emit `work-item-banner-dismissed` event when cross icon is clicked', () => { - findAlert().vm.$emit('dismiss'); - expect(wrapper.emitted('work-item-banner-dismissed').length).toBe(1); - }); - - it('the alert variant should be tip', () => { - expect(findAlert().props('variant')).toBe('tip'); - }); - - it('should have the correct text for title', () => { - expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle); - }); - - it('should have the correct link to work item link', () => { - expect(findHelpLink().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe(tasksHelpPath); - }); -}); 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 9f7659b3f8d..083bb5bc4a4 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; +import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; @@ -49,6 +49,7 @@ describe('WorkItemLabels component', () => { searchQueryHandler = successSearchQueryHandler, updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, fetchByIid = false, + queryVariables = { id: workItemId }, } = {}) => { const apolloProvider = createMockApollo([ [workItemQuery, workItemQueryHandler], @@ -63,9 +64,7 @@ describe('WorkItemLabels component', () => { workItemId, canUpdate, fullPath: 'test-project-path', - queryVariables: { - id: workItemId, - }, + queryVariables, fetchByIid, }, attachTo: document.body, @@ -251,4 +250,11 @@ describe('WorkItemLabels component', () => { expect(workItemQuerySuccess).not.toHaveBeenCalled(); expect(workItemByIidResponseHandler).toHaveBeenCalled(); }); + + it('skips calling the handlers when missing the needed queryVariables', async () => { + createComponent({ queryVariables: {}, fetchByIid: false }); + await waitForPromises(); + + expect(workItemQuerySuccess).not.toHaveBeenCalled(); + }); }); 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/okr_actions_split_button_spec.js new file mode 100644 index 00000000000..5fbd8e7e1a7 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js @@ -0,0 +1,35 @@ +import { GlDropdownSectionHeader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; + +import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +const createComponent = () => { + return extendedWrapper(shallowMount(OkrActionsSplitButton)); +}; + +describe('RelatedItemsTree', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('OkrActionsSplitButton', () => { + describe('template', () => { + it('renders objective and key results sections', () => { + expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toContain( + 'Objective', + ); + + expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(1).text()).toContain( + 'Key result', + ); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js new file mode 100644 index 00000000000..47489d4796b --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js @@ -0,0 +1,67 @@ +import { GlLabel, GlAvatarsInline } from '@gitlab/ui'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import ItemMilestone from '~/issuable/components/issue_milestone.vue'; +import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue'; + +import { mockMilestone, mockAssignees, mockLabels } from '../../mock_data'; + +describe('WorkItemLinkChildMetadata', () => { + let wrapper; + + const createComponent = ({ + allowsScopedLabels = true, + milestone = mockMilestone, + assignees = mockAssignees, + labels = mockLabels, + } = {}) => { + wrapper = shallowMountExtended(WorkItemLinkChildMetadata, { + propsData: { + allowsScopedLabels, + milestone, + assignees, + labels, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('renders milestone link button', () => { + const milestoneLink = wrapper.findComponent(ItemMilestone); + + expect(milestoneLink.exists()).toBe(true); + expect(milestoneLink.props('milestone')).toEqual(mockMilestone); + }); + + it('renders avatars for assignees', () => { + const avatars = wrapper.findComponent(GlAvatarsInline); + + expect(avatars.exists()).toBe(true); + expect(avatars.props()).toMatchObject({ + avatars: mockAssignees, + collapsed: true, + maxVisible: 2, + avatarSize: 24, + badgeTooltipProp: 'name', + badgeSrOnlyText: '', + }); + }); + + it('renders labels', () => { + const labels = wrapper.findAllComponents(GlLabel); + const mockLabel = mockLabels[0]; + + expect(labels).toHaveLength(mockLabels.length); + expect(labels.at(0).props()).toMatchObject({ + title: mockLabel.title, + backgroundColor: mockLabel.color, + description: mockLabel.description, + scoped: false, + }); + expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped + }); +}); 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 1d5472a0473..73d498ad055 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 @@ -1,33 +1,73 @@ -import { GlButton, GlIcon } from '@gitlab/ui'; +import { GlIcon } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; +import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql'; +import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; +import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue'; +import { + WIDGET_TYPE_HIERARCHY, + TASK_TYPE_NAME, + WORK_ITEM_TYPE_VALUE_OBJECTIVE, +} from '~/work_items/constants'; -import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data'; +import { + workItemTask, + workItemObjectiveWithChild, + workItemObjectiveNoMetadata, + confidentialWorkItemTask, + closedWorkItemTask, + mockMilestone, + mockAssignees, + mockLabels, + workItemHierarchyTreeResponse, + workItemHierarchyTreeFailureResponse, +} from '../../mock_data'; + +jest.mock('~/flash'); describe('WorkItemLinkChild', () => { const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; let wrapper; + let getWorkItemTreeQueryHandler; + + Vue.use(VueApollo); const createComponent = ({ projectPath = 'gitlab-org/gitlab-test', canUpdate = true, issuableGid = WORK_ITEM_ID, childItem = workItemTask, + workItemType = TASK_TYPE_NAME, + apolloProvider = null, } = {}) => { + getWorkItemTreeQueryHandler = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse); + wrapper = shallowMountExtended(WorkItemLinkChild, { + apolloProvider: + apolloProvider || createMockApollo([[getWorkItemTreeQuery, getWorkItemTreeQueryHandler]]), propsData: { projectPath, canUpdate, issuableGid, childItem, + workItemType, }, }); }; + beforeEach(() => { + createAlert.mockClear(); + }); + afterEach(() => { wrapper.destroy(); }); @@ -66,7 +106,7 @@ describe('WorkItemLinkChild', () => { beforeEach(() => { createComponent(); - titleEl = wrapper.findComponent(GlButton); + titleEl = wrapper.findByTestId('item-title'); }); it('renders item title', () => { @@ -76,16 +116,52 @@ describe('WorkItemLinkChild', () => { it.each` action | event | emittedEvent - ${'clicking'} | ${'click'} | ${'click'} ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'} ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'} `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => { + titleEl.vm.$emit(event); + + expect(wrapper.emitted(emittedEvent)).toEqual([[]]); + }); + + it('emits click event with correct parameters on clicking title', () => { const eventObj = { preventDefault: jest.fn(), }; - titleEl.vm.$emit(event, eventObj); + titleEl.vm.$emit('click', eventObj); - expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]); + expect(wrapper.emitted('click')).toEqual([[eventObj]]); + }); + }); + + describe('item metadata', () => { + const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata); + + beforeEach(() => { + createComponent({ + childItem: workItemObjectiveWithChild, + workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, + }); + }); + + it('renders item metadata component when item has metadata present', () => { + const metadataEl = findMetadataComponent(); + expect(metadataEl.exists()).toBe(true); + expect(metadataEl.props()).toMatchObject({ + allowsScopedLabels: true, + milestone: mockMilestone, + assignees: mockAssignees, + labels: mockLabels, + }); + }); + + it('does not render item metadata component when item has no metadata present', () => { + createComponent({ + childItem: workItemObjectiveNoMetadata, + workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, + }); + + expect(findMetadataComponent().exists()).toBe(false); }); }); @@ -116,7 +192,78 @@ describe('WorkItemLinkChild', () => { it('removeChild event on menu triggers `click-remove-child` event', () => { itemMenuEl.vm.$emit('removeChild'); - expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]); + expect(wrapper.emitted('removeChild')).toEqual([[workItemTask.id]]); + }); + }); + + describe('nested children', () => { + const findExpandButton = () => wrapper.findByTestId('expand-child'); + const findTreeChildren = () => wrapper.findComponent(WorkItemTreeChildren); + + beforeEach(() => { + getWorkItemTreeQueryHandler.mockClear(); + createComponent({ + childItem: workItemObjectiveWithChild, + workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, + }); + }); + + it('displays expand button when item has children, children are not displayed by default', () => { + expect(findExpandButton().exists()).toBe(true); + expect(findTreeChildren().exists()).toBe(false); + }); + + it('fetches and displays children of item when clicking on expand button', async () => { + await findExpandButton().vm.$emit('click'); + + expect(findExpandButton().props('loading')).toBe(true); + await waitForPromises(); + + expect(getWorkItemTreeQueryHandler).toHaveBeenCalled(); + expect(findTreeChildren().exists()).toBe(true); + + const widgetHierarchy = workItemHierarchyTreeResponse.data.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_HIERARCHY, + ); + expect(findTreeChildren().props('children')).toEqual(widgetHierarchy.children.nodes); + }); + + it('does not fetch children if already fetched once while clicking expand button', async () => { + findExpandButton().vm.$emit('click'); // Expand for the first time + await waitForPromises(); + + expect(findTreeChildren().exists()).toBe(true); + + await findExpandButton().vm.$emit('click'); // Collapse + findExpandButton().vm.$emit('click'); // Expand again + await waitForPromises(); + + expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once. + expect(findTreeChildren().exists()).toBe(true); + }); + + it('calls createAlert when children fetch request fails on clicking expand button', async () => { + const getWorkItemTreeQueryFailureHandler = jest + .fn() + .mockRejectedValue(workItemHierarchyTreeFailureResponse); + const apolloProvider = createMockApollo([ + [getWorkItemTreeQuery, getWorkItemTreeQueryFailureHandler], + ]); + + createComponent({ + childItem: workItemObjectiveWithChild, + workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE, + apolloProvider, + }); + + findExpandButton().vm.$emit('click'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + captureError: true, + error: expect.any(Object), + message: 'Something went wrong while fetching children.', + }); }); }); }); 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 071d5fb715a..bbe460a55ba 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 @@ -33,7 +33,7 @@ describe('WorkItemLinksForm', () => { typesResponse = projectWorkItemTypesQueryResponse, parentConfidential = false, hasIterationsFeature = false, - workItemsMvc2Enabled = false, + workItemsMvcEnabled = false, parentIteration = null, formType = FORM_TYPES.create, } = {}) => { @@ -52,7 +52,7 @@ describe('WorkItemLinksForm', () => { }, provide: { glFeatures: { - workItemsMvc2: workItemsMvc2Enabled, + workItemsMvc: workItemsMvcEnabled, }, projectPath: 'project/path', hasIterationsFeature, @@ -165,23 +165,8 @@ describe('WorkItemLinksForm', () => { }); describe('associate iteration with task', () => { - it('does not update iteration when mvc2 feature flag is not enabled', async () => { - await createComponent({ - hasIterationsFeature: true, - parentIteration: mockParentIteration, - }); - - findInput().vm.$emit('input', 'Create task test'); - - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); - await waitForPromises(); - expect(updateMutationResolver).not.toHaveBeenCalled(); - }); it('updates when parent has an iteration associated', async () => { await createComponent({ - workItemsMvc2Enabled: true, hasIterationsFeature: true, parentIteration: mockParentIteration, }); @@ -191,18 +176,23 @@ describe('WorkItemLinksForm', () => { preventDefault: jest.fn(), }); await waitForPromises(); - expect(updateMutationResolver).toHaveBeenCalledWith({ + expect(createMutationResolver).toHaveBeenCalledWith({ input: { - id: 'gid://gitlab/WorkItem/1', + title: 'Create task test', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: false, iterationWidget: { iterationId: mockParentIteration.id, }, }, }); }); - it('does not update when parent has no iteration associated', async () => { + it('does not send the iteration widget to mutation when parent has no iteration associated', async () => { await createComponent({ - workItemsMvc2Enabled: true, hasIterationsFeature: true, }); findInput().vm.$emit('input', 'Create task test'); @@ -211,7 +201,20 @@ describe('WorkItemLinksForm', () => { preventDefault: jest.fn(), }); await waitForPromises(); - expect(updateMutationResolver).not.toHaveBeenCalled(); + expect(createMutationResolver).not.toHaveBeenCalledWith({ + input: { + title: 'Create task test', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: false, + iterationWidget: { + iterationId: mockParentIteration.id, + }, + }, + }); }); }); }); 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 66ce2c1becf..a61de78c623 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 @@ -4,20 +4,25 @@ import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { stubComponent } from 'helpers/stub_component'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; +import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import { FORM_TYPES } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { workItemHierarchyResponse, workItemHierarchyEmptyResponse, workItemHierarchyNoUpdatePermissionResponse, changeWorkItemParentMutationResponse, workItemQueryResponse, + projectWorkItemResponse, } from '../../mock_data'; Vue.use(VueApollo); @@ -55,6 +60,7 @@ const issueDetailsResponse = (confidential = false) => ({ }, }, }); +const showModal = jest.fn(); describe('WorkItemLinks', () => { let wrapper; @@ -71,6 +77,7 @@ describe('WorkItemLinks', () => { .mockResolvedValue(changeWorkItemParentMutationResponse); const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const childWorkItemByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); const createComponent = async ({ data = {}, @@ -78,6 +85,7 @@ describe('WorkItemLinks', () => { mutationHandler = mutationChangeParentHandler, issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()), hasIterationsFeature = false, + fetchByIid = false, } = {}) => { mockApollo = createMockApollo( [ @@ -85,6 +93,7 @@ describe('WorkItemLinks', () => { [changeWorkItemParentMutation, mutationHandler], [workItemQuery, childWorkItemQueryHandler], [issueDetailsQuery, issueDetailsQueryHandler], + [workItemByIidQuery, childWorkItemByIidHandler], ], {}, { addTypename: true }, @@ -100,12 +109,22 @@ describe('WorkItemLinks', () => { projectPath: 'project/path', iid: '1', hasIterationsFeature, + glFeatures: { + useIidInWorkItemsPath: fetchByIid, + }, }, propsData: { issuableId: 1 }, apolloProvider: mockApollo, mocks: { $toast, }, + stubs: { + WorkItemDetailModal: stubComponent(WorkItemDetailModal, { + methods: { + show: showModal, + }, + }), + }, }); await waitForPromises(); @@ -130,6 +149,7 @@ describe('WorkItemLinks', () => { afterEach(() => { wrapper.destroy(); mockApollo = null; + setWindowLocation(''); }); it('is expanded by default', () => { @@ -237,7 +257,7 @@ describe('WorkItemLinks', () => { }); it('calls correct mutation with correct variables', async () => { - firstChild.vm.$emit('remove', firstChild.vm.childItem.id); + firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id); await waitForPromises(); @@ -252,7 +272,7 @@ describe('WorkItemLinks', () => { }); it('shows toast when mutation succeeds', async () => { - firstChild.vm.$emit('remove', firstChild.vm.childItem.id); + firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id); await waitForPromises(); @@ -264,56 +284,164 @@ describe('WorkItemLinks', () => { it('renders correct number of children after removal', async () => { expect(findWorkItemLinkChildItems()).toHaveLength(4); - firstChild.vm.$emit('remove', firstChild.vm.childItem.id); + firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id); await waitForPromises(); expect(findWorkItemLinkChildItems()).toHaveLength(3); }); }); - describe('prefetching child items', () => { - let firstChild; - - beforeEach(async () => { - await createComponent(); + describe('when parent item is confidential', () => { + it('passes correct confidentiality status to form', async () => { + await createComponent({ + issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)), + }); + findToggleFormDropdown().vm.$emit('click'); + findToggleAddFormButton().vm.$emit('click'); + await nextTick(); - firstChild = findFirstWorkItemLinkChild(); + expect(findAddLinksForm().props('parentConfidential')).toBe(true); }); + }); - it('does not fetch the child work item before hovering work item links', () => { - expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + describe('when work item is fetched by id', () => { + describe('prefetching child items', () => { + let firstChild; + + beforeEach(async () => { + await createComponent(); + + firstChild = findFirstWorkItemLinkChild(); + }); + + it('does not fetch the child work item by id before hovering work item links', () => { + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); + + it('fetches the child work item by id if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemQueryHandler).toHaveBeenCalledWith({ + id: 'gid://gitlab/WorkItem/2', + }); + }); + + it('does not fetch the child work item by id if link is hovered for less than 250 ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(200); + firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); + await waitForPromises(); + + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); + + it('does not fetch work item by iid if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemByIidHandler).not.toHaveBeenCalled(); + }); }); - it('fetches the child work item if link is hovered for 250+ ms', async () => { - firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); - jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); - await waitForPromises(); + it('starts prefetching work item by id if URL contains work item id', async () => { + setWindowLocation('?work_item_id=5'); + await createComponent(); expect(childWorkItemQueryHandler).toHaveBeenCalledWith({ - id: 'gid://gitlab/WorkItem/2', + id: 'gid://gitlab/WorkItem/5', }); }); - it('does not fetch the child work item if link is hovered for less than 250 ms', async () => { - firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); - jest.advanceTimersByTime(200); - firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); - await waitForPromises(); + it('does not open the modal if work item id URL parameter is not found in child items', async () => { + setWindowLocation('?work_item_id=555'); + await createComponent(); + + expect(showModal).not.toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(null); + }); + + it('opens the modal if work item id URL parameter is found in child items', async () => { + setWindowLocation('?work_item_id=2'); + await createComponent(); - expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + expect(showModal).toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe( + 'gid://gitlab/WorkItem/2', + ); }); }); - describe('when parent item is confidential', () => { - it('passes correct confidentiality status to form', async () => { - await createComponent({ - issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)), + describe('when work item is fetched by iid', () => { + describe('prefetching child items', () => { + let firstChild; + + beforeEach(async () => { + setWindowLocation('?iid_path=true'); + await createComponent({ fetchByIid: true }); + + firstChild = findFirstWorkItemLinkChild(); }); - findToggleFormDropdown().vm.$emit('click'); - findToggleAddFormButton().vm.$emit('click'); - await nextTick(); - expect(findAddLinksForm().props('parentConfidential')).toBe(true); + it('does not fetch the child work item by iid before hovering work item links', () => { + expect(childWorkItemByIidHandler).not.toHaveBeenCalled(); + }); + + it('fetches the child work item by iid if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemByIidHandler).toHaveBeenCalledWith({ + fullPath: 'project/path', + iid: '2', + }); + }); + + it('does not fetch the child work item by iid if link is hovered for less than 250 ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(200); + firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); + await waitForPromises(); + + expect(childWorkItemByIidHandler).not.toHaveBeenCalled(); + }); + + it('does not fetch work item by id if link is hovered for 250+ ms', async () => { + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + await waitForPromises(); + + expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); + }); }); + + it('starts prefetching work item by iid if URL contains work item id', async () => { + setWindowLocation('?work_item_iid=5&iid_path=true'); + await createComponent({ fetchByIid: true }); + + expect(childWorkItemByIidHandler).toHaveBeenCalledWith({ + iid: '5', + fullPath: 'project/path', + }); + }); + }); + + it('does not open the modal if work item iid URL parameter is not found in child items', async () => { + setWindowLocation('?work_item_iid=555&iid_path=true'); + await createComponent({ fetchByIid: true }); + + expect(showModal).not.toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null); + }); + + it('opens the modal if work item iid URL parameter is found in child items', async () => { + setWindowLocation('?work_item_iid=2&iid_path=true'); + await createComponent({ fetchByIid: true }); + + expect(showModal).toHaveBeenCalled(); + expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2'); }); }); 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 new file mode 100644 index 00000000000..96211e12755 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -0,0 +1,147 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; + +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; +import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; +import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; +import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; + +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; + +import { + FORM_TYPES, + WORK_ITEM_TYPE_ENUM_OBJECTIVE, + WORK_ITEM_TYPE_ENUM_KEY_RESULT, +} from '~/work_items/constants'; +import { childrenWorkItems, workItemObjectiveWithChild } from '../../mock_data'; + +describe('WorkItemTree', () => { + let getWorkItemQueryHandler; + let wrapper; + + const findToggleButton = () => wrapper.findByTestId('toggle-tree'); + const findTreeBody = () => wrapper.findByTestId('tree-body'); + const findEmptyState = () => wrapper.findByTestId('tree-empty'); + const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton); + const findForm = () => wrapper.findComponent(WorkItemLinksForm); + const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); + + Vue.use(VueApollo); + + const createComponent = ({ + workItemType = 'Objective', + children = childrenWorkItems, + apolloProvider = null, + } = {}) => { + const mockWorkItemResponse = { + data: { + workItem: { + ...workItemObjectiveWithChild, + workItemType: { + ...workItemObjectiveWithChild.workItemType, + name: workItemType, + }, + }, + }, + }; + getWorkItemQueryHandler = jest.fn().mockResolvedValue(mockWorkItemResponse); + + wrapper = shallowMountExtended(WorkItemTree, { + apolloProvider: + apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]), + propsData: { + workItemType, + workItemId: 'gid://gitlab/WorkItem/515', + children, + projectPath: 'test/project', + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('is expanded by default and displays Add button', () => { + expect(findToggleButton().props('icon')).toBe('chevron-lg-up'); + expect(findTreeBody().exists()).toBe(true); + expect(findToggleFormSplitButton().exists()).toBe(true); + }); + + it('collapses on click toggle button', async () => { + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findToggleButton().props('icon')).toBe('chevron-lg-down'); + expect(findTreeBody().exists()).toBe(false); + }); + + it('displays empty state if there are no children', () => { + createComponent({ children: [] }); + expect(findEmptyState().exists()).toBe(true); + }); + + it('renders all hierarchy widget children', () => { + expect(findWorkItemLinkChildItems()).toHaveLength(4); + }); + + it('does not display form by default', () => { + expect(findForm().exists()).toBe(false); + }); + + 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} + `( + 'when selecting $option from split button, renders the form passing $formType and $childType', + async ({ event, formType, childType }) => { + findToggleFormSplitButton().vm.$emit(event); + await nextTick(); + + expect(findForm().exists()).toBe(true); + expect(findForm().props('formType')).toBe(formType); + expect(findForm().props('childrenType')).toBe(childType); + }, + ); + + it('remove event on child triggers `removeChild` event', () => { + const firstChild = findWorkItemLinkChildItems().at(0); + firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2'); + + expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]); + }); + + it.each` + description | workItemType | prefetch + ${'prefetches'} | ${'Issue'} | ${true} + ${'does not prefetch'} | ${'Objective'} | ${false} + `( + '$description work-item-link-child on mouseover when workItemType is "$workItemType"', + async ({ workItemType, prefetch }) => { + createComponent({ workItemType }); + const firstChild = findWorkItemLinkChildItems().at(0); + firstChild.vm.$emit('mouseover', childrenWorkItems[0]); + await nextTick(); + await waitForPromises(); + + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + + if (prefetch) { + expect(getWorkItemQueryHandler).toHaveBeenCalled(); + } else { + expect(getWorkItemQueryHandler).not.toHaveBeenCalled(); + } + }, + ); +}); 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 60ba2b55f76..5997de01274 100644 --- a/spec/frontend/work_items/components/work_item_milestone_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_spec.js @@ -179,6 +179,18 @@ describe('WorkItemMilestone component', () => { createComponent({ canUpdate: true }); }); + it('calls successSearchQueryHandler with variables when dropdown is opened', async () => { + showDropdown(); + await nextTick(); + + expect(successSearchQueryHandler).toHaveBeenCalledWith({ + first: 20, + fullPath: 'full-path', + state: 'active', + title: '', + }); + }); + it('shows the skeleton loader when the items are being fetched on click', async () => { showDropdown(); await nextTick(); diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js new file mode 100644 index 00000000000..ed68d214fc9 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -0,0 +1,107 @@ +import { GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import SystemNote from '~/work_items/components/notes/system_note.vue'; +import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; +import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql'; +import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql'; +import { WIDGET_TYPE_NOTES } from '~/work_items/constants'; +import { + mockWorkItemNotesResponse, + workItemQueryResponse, + mockWorkItemNotesByIidResponse, +} from '../mock_data'; + +const mockWorkItemId = workItemQueryResponse.data.workItem.id; +const mockNotesWidgetResponse = mockWorkItemNotesResponse.data.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_NOTES, +); + +const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspace.workItems.nodes[0].widgets.find( + (widget) => widget.type === WIDGET_TYPE_NOTES, +); + +describe('WorkItemNotes component', () => { + let wrapper; + + Vue.use(VueApollo); + + const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote); + const findActivityLabel = () => wrapper.find('label'); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse); + const workItemNotesByIidQueryHandler = jest + .fn() + .mockResolvedValue(mockWorkItemNotesByIidResponse); + + const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false } = {}) => { + wrapper = shallowMount(WorkItemNotes, { + apolloProvider: createMockApollo([ + [workItemNotesQuery, workItemNotesQueryHandler], + [workItemNotesByIidQuery, workItemNotesByIidQueryHandler], + ]), + propsData: { + workItemId, + queryVariables: { + id: workItemId, + }, + fullPath: 'test-path', + fetchByIid, + }, + provide: { + glFeatures: { + useIidInWorkItemsPath: fetchByIid, + }, + }, + }); + }; + + beforeEach(async () => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders activity label', () => { + expect(findActivityLabel().exists()).toBe(true); + }); + + describe('when notes are loading', () => { + it('renders skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('does not render system notes', () => { + expect(findAllSystemNotes().exists()).toBe(false); + }); + }); + + describe('when notes have been loaded', () => { + it('does not render skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('renders system notes to the length of the response', async () => { + await waitForPromises(); + expect(findAllSystemNotes()).toHaveLength(mockNotesWidgetResponse.discussions.nodes.length); + }); + }); + + describe('when the notes are fetched by `iid`', () => { + beforeEach(async () => { + createComponent({ workItemId: mockWorkItemId, fetchByIid: true }); + await waitForPromises(); + }); + + it('shows the notes list', () => { + expect(findAllSystemNotes()).toHaveLength( + mockNotesByIidWidgetResponse.discussions.nodes.length, + ); + }); + }); +}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 635a1f326f8..850672b68d0 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -36,6 +36,16 @@ export const mockLabels = [ }, ]; +export const mockMilestone = { + __typename: 'Milestone', + id: 'gid://gitlab/Milestone/30', + title: 'v4.0', + state: 'active', + expired: false, + startDate: '2022-10-17', + dueDate: '2022-10-24', +}; + export const workItemQueryResponse = { data: { workItem: { @@ -85,11 +95,18 @@ export const workItemQueryResponse = { { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', + hasChildren: true, parent: { id: 'gid://gitlab/Issue/1', iid: '5', title: 'Parent title', confidential: false, + webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/1', + name: 'Issue', + iconName: 'issue-type-issue', + }, }, children: { nodes: [ @@ -97,6 +114,20 @@ export const workItemQueryResponse = { id: 'gid://gitlab/WorkItem/444', createdAt: '2022-08-03T12:41:54Z', closedAt: null, + confidential: false, + title: '123', + state: 'OPEN', + workItemType: { + id: '1', + name: 'Task', + iconName: 'issue-type-task', + }, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: false, + }, + ], }, ], }, @@ -138,13 +169,25 @@ export const updateWorkItemMutationResponse = { }, widgets: [ { + type: 'HIERARCHY', children: { nodes: [ { id: 'gid://gitlab/WorkItem/444', + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + confidential: false, + title: '123', + state: 'OPEN', + workItemType: { + id: '1', + name: 'Task', + iconName: 'issue-type-task', + }, }, ], }, + __typename: 'WorkItemConnection', }, { __typename: 'WorkItemWidgetAssignees', @@ -177,6 +220,12 @@ export const mockParent = { iid: '5', title: 'Parent title', confidential: false, + webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/1', + name: 'Issue', + iconName: 'issue-type-issue', + }, }, }; @@ -193,6 +242,20 @@ export const descriptionHtmlWithCheckboxes = ` </ul> `; +const taskType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', +}; + +export const objectiveType = { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', +}; + export const workItemResponseFactory = ({ canUpdate = false, canDelete = false, @@ -201,8 +264,10 @@ export const workItemResponseFactory = ({ datesWidgetPresent = true, labelsWidgetPresent = true, weightWidgetPresent = true, + progressWidgetPresent = true, milestoneWidgetPresent = true, iterationWidgetPresent = true, + healthStatusWidgetPresent = true, confidential = false, canInviteMembers = false, allowsScopedLabels = false, @@ -210,6 +275,7 @@ export const workItemResponseFactory = ({ lastEditedBy = null, withCheckboxes = false, parent = mockParent.parent, + workItemType = taskType, } = {}) => ({ data: { workItem: { @@ -227,12 +293,7 @@ export const workItemResponseFactory = ({ id: '1', fullPath: 'test-project-path', }, - workItemType: { - __typename: 'WorkItemType', - id: 'gid://gitlab/WorkItems::Type/5', - name: 'Task', - iconName: 'issue-type-task', - }, + workItemType, userPermissions: { deleteWorkItem: canDelete, updateWorkItem: canUpdate, @@ -298,26 +359,51 @@ export const workItemResponseFactory = ({ }, } : { type: 'MOCK TYPE' }, + progressWidgetPresent + ? { + __typename: 'WorkItemWidgetProgress', + type: 'PROGRESS', + progress: 0, + } + : { type: 'MOCK TYPE' }, milestoneWidgetPresent ? { __typename: 'WorkItemWidgetMilestone', type: 'MILESTONE', - milestone: { - expired: false, - id: 'gid://gitlab/Milestone/30', - title: 'v4.0', - }, + milestone: mockMilestone, + } + : { type: 'MOCK TYPE' }, + healthStatusWidgetPresent + ? { + __typename: 'WorkItemWidgetHealthStatus', + type: 'HEALTH_STATUS', + healthStatus: 'onTrack', } : { type: 'MOCK TYPE' }, { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', + hasChildren: true, children: { nodes: [ { id: 'gid://gitlab/WorkItem/444', createdAt: '2022-08-03T12:41:54Z', closedAt: null, + confidential: false, + title: '123', + state: 'OPEN', + workItemType: { + id: '1', + name: 'Task', + iconName: 'issue-type-task', + }, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: false, + }, + ], }, ], }, @@ -637,6 +723,8 @@ export const workItemHierarchyEmptyResponse = { id: 'gid://gitlab/WorkItem/1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', + name: 'Issue', + iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', @@ -660,6 +748,7 @@ export const workItemHierarchyEmptyResponse = { { type: 'HIERARCHY', parent: null, + hasChildren: false, children: { nodes: [], __typename: 'WorkItemConnection', @@ -678,6 +767,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = { id: 'gid://gitlab/WorkItem/1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', + name: 'Issue', + iconName: 'issue-type-issue', __typename: 'WorkItemType', }, title: 'New title', @@ -699,12 +790,16 @@ export const workItemHierarchyNoUpdatePermissionResponse = { { type: 'HIERARCHY', parent: null, + hasChildren: true, children: { nodes: [ { id: 'gid://gitlab/WorkItem/2', + iid: '2', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'xyz', @@ -712,6 +807,12 @@ export const workItemHierarchyNoUpdatePermissionResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: false, + }, + ], __typename: 'WorkItem', }, ], @@ -727,8 +828,11 @@ export const workItemHierarchyNoUpdatePermissionResponse = { export const workItemTask = { id: 'gid://gitlab/WorkItem/4', + iid: '4', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'bar', @@ -741,8 +845,11 @@ export const workItemTask = { export const confidentialWorkItemTask = { id: 'gid://gitlab/WorkItem/2', + iid: '2', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'xyz', @@ -755,8 +862,11 @@ export const confidentialWorkItemTask = { export const closedWorkItemTask = { id: 'gid://gitlab/WorkItem/3', + iid: '3', workItemType: { id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', __typename: 'WorkItemType', }, title: 'abc', @@ -767,12 +877,153 @@ export const closedWorkItemTask = { __typename: 'WorkItem', }; +export const childrenWorkItems = [ + confidentialWorkItemTask, + closedWorkItemTask, + workItemTask, + { + id: 'gid://gitlab/WorkItem/5', + iid: '5', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', + __typename: 'WorkItemType', + }, + title: 'foobar', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', + }, +]; + export const workItemHierarchyResponse = { data: { workItem: { id: 'gid://gitlab/WorkItem/1', + iid: '1', workItemType: { id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + title: 'New title', + userPermissions: { + deleteWorkItem: true, + updateWorkItem: true, + }, + confidential: false, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, + widgets: [ + { + type: 'DESCRIPTION', + __typename: 'WorkItemWidgetDescription', + }, + { + type: 'HIERARCHY', + parent: null, + hasChildren: true, + children: { + nodes: childrenWorkItems, + __typename: 'WorkItemConnection', + }, + __typename: 'WorkItemWidgetHierarchy', + }, + ], + __typename: 'WorkItem', + }, + }, +}; + +export const workItemObjectiveWithChild = { + id: 'gid://gitlab/WorkItem/12', + iid: '12', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, + userPermissions: { + deleteWorkItem: true, + updateWorkItem: true, + }, + title: 'Objective', + description: 'Objective description', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: true, + parent: null, + children: { + nodes: [], + }, + __typename: 'WorkItemWidgetHierarchy', + }, + { + type: 'MILESTONE', + __typename: 'WorkItemWidgetMilestone', + milestone: mockMilestone, + }, + { + type: 'ASSIGNEES', + __typename: 'WorkItemWidgetAssignees', + canInviteMembers: true, + allowsMultipleAssignees: true, + assignees: { + __typename: 'UserCoreConnection', + nodes: mockAssignees, + }, + }, + { + type: 'LABELS', + __typename: 'WorkItemWidgetLabels', + allowsScopedLabels: true, + labels: { + __typename: 'LabelConnection', + nodes: mockLabels, + }, + }, + ], + __typename: 'WorkItem', +}; + +export const workItemObjectiveNoMetadata = { + ...workItemObjectiveWithChild, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: true, + __typename: 'WorkItemWidgetHierarchy', + }, + ], +}; + +export const workItemHierarchyTreeResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/2', + iid: '2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', __typename: 'WorkItemType', }, title: 'New title', @@ -794,22 +1045,30 @@ export const workItemHierarchyResponse = { { type: 'HIERARCHY', parent: null, + hasChildren: true, children: { nodes: [ - confidentialWorkItemTask, - closedWorkItemTask, - workItemTask, { - id: 'gid://gitlab/WorkItem/5', + id: 'gid://gitlab/WorkItem/13', + iid: '13', workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', + id: 'gid://gitlab/WorkItems::Type/2411', + name: 'Objective', + iconName: 'issue-type-objective', __typename: 'WorkItemType', }, - title: 'foobar', + title: 'Objective 2', state: 'OPEN', confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + widgets: [ + { + type: 'HIERARCHY', + hasChildren: true, + __typename: 'WorkItemWidgetHierarchy', + }, + ], __typename: 'WorkItem', }, ], @@ -823,6 +1082,15 @@ export const workItemHierarchyResponse = { }, }; +export const workItemHierarchyTreeFailureResponse = { + data: {}, + errors: [ + { + message: 'Something went wrong', + }, + ], +}; + export const changeWorkItemParentMutationResponse = { data: { workItemUpdate: { @@ -856,6 +1124,7 @@ export const changeWorkItemParentMutationResponse = { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', parent: null, + hasChildren: false, children: { nodes: [], }, @@ -1196,3 +1465,288 @@ export const projectWorkItemResponse = { }, }, }; + +export const mockWorkItemNotesResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/600', + iid: '60', + widgets: [ + { + __typename: 'WorkItemWidgetIteration', + }, + { + __typename: 'WorkItemWidgetWeight', + }, + { + __typename: 'WorkItemWidgetAssignees', + }, + { + __typename: 'WorkItemWidgetLabels', + }, + { + __typename: 'WorkItemWidgetDescription', + }, + { + __typename: 'WorkItemWidgetHierarchy', + }, + { + __typename: 'WorkItemWidgetStartAndDueDate', + }, + { + __typename: 'WorkItemWidgetMilestone', + }, + { + type: 'NOTES', + discussions: { + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + __typename: 'PageInfo', + }, + nodes: [ + { + id: + 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e', + notes: { + nodes: [ + { + id: 'gid://gitlab/Note/2428', + body: 'added #31 as parent issue', + bodyHtml: + '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>', + systemNoteIconName: 'link', + createdAt: '2022-11-14T04:18:59Z', + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + { + id: + 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc', + notes: { + nodes: [ + { + id: 'gid://gitlab/MilestoneNote/not-persisted', + body: 'changed milestone to %5', + bodyHtml: + '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>', + systemNoteIconName: 'clock', + createdAt: '2022-11-14T04:18:59Z', + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + { + id: + 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864', + notes: { + nodes: [ + { + id: 'gid://gitlab/WeightNote/not-persisted', + body: 'changed weight to 89', + bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>', + systemNoteIconName: 'weight', + createdAt: '2022-11-25T07:16:20Z', + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + ], + __typename: 'DiscussionConnection', + }, + __typename: 'WorkItemWidgetNotes', + }, + ], + __typename: 'WorkItem', + }, + }, +}; +export const mockWorkItemNotesByIidResponse = { + data: { + workspace: { + id: 'gid://gitlab/Project/6', + workItems: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/600', + iid: '51', + widgets: [ + { + __typename: 'WorkItemWidgetIteration', + }, + { + __typename: 'WorkItemWidgetWeight', + }, + { + __typename: 'WorkItemWidgetHealthStatus', + }, + { + __typename: 'WorkItemWidgetAssignees', + }, + { + __typename: 'WorkItemWidgetLabels', + }, + { + __typename: 'WorkItemWidgetDescription', + }, + { + __typename: 'WorkItemWidgetHierarchy', + }, + { + __typename: 'WorkItemWidgetStartAndDueDate', + }, + { + __typename: 'WorkItemWidgetMilestone', + }, + { + type: 'NOTES', + discussions: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: null, + endCursor: + 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0xNCAwNDoxOTowMC4wOTkxMTcwMDAgKzAwMDAiLCJpZCI6IjQyNyIsIl9rZCI6Im4ifQ==', + __typename: 'PageInfo', + }, + nodes: [ + { + id: + 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e', + notes: { + nodes: [ + { + id: 'gid://gitlab/Note/2428', + body: 'added #31 as parent issue', + bodyHtml: + '\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e', + systemNoteIconName: 'link', + createdAt: '2022-11-14T04:18:59Z', + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + { + id: + 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc', + notes: { + nodes: [ + { + id: + 'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc', + body: 'changed milestone to %5', + bodyHtml: + '\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e', + systemNoteIconName: 'clock', + createdAt: '2022-11-14T04:18:59Z', + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + { + id: + 'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3', + notes: { + nodes: [ + { + id: + 'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3', + body: 'changed iteration to *iteration:5352', + bodyHtml: + '\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e', + systemNoteIconName: 'iteration', + createdAt: '2022-11-14T04:19:00Z', + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + ], + __typename: 'DiscussionConnection', + }, + __typename: 'WorkItemWidgetNotes', + }, + ], + __typename: 'WorkItem', + }, + ], + __typename: 'WorkItemConnection', + }, + __typename: 'Project', + }, + }, +}; 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 880c4271024..a766962771a 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -55,7 +55,7 @@ describe('Work items root component', () => { isModal: false, workItemId: 'gid://gitlab/WorkItem/1', workItemParentId: null, - iid: '1', + workItemIid: '1', }); }); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 982f9f71f9e..b503d819435 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -1,14 +1,12 @@ import { mount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import { workItemAssigneesSubscriptionResponse, workItemDatesSubscriptionResponse, workItemResponseFactory, workItemTitleSubscriptionResponse, - workItemWeightSubscriptionResponse, workItemLabelsSubscriptionResponse, workItemMilestoneSubscriptionResponse, workItemDescriptionSubscriptionResponse, @@ -25,6 +23,8 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import { createRouter } from '~/work_items/router'; +jest.mock('~/behaviors/markdown/render_gfm'); + describe('Work items router', () => { let wrapper; @@ -33,7 +33,6 @@ describe('Work items router', () => { const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory()); const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); - const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); const assigneesSubscriptionHandler = jest .fn() .mockResolvedValue(workItemAssigneesSubscriptionResponse); @@ -61,10 +60,6 @@ describe('Work items router', () => { [workItemDescriptionSubscription, descriptionSubscriptionHandler], ]; - if (IS_EE) { - handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); - } - wrapper = mount(App, { apolloProvider: createMockApollo(handlers), router, @@ -72,6 +67,13 @@ describe('Work items router', () => { fullPath: 'full-path', issuesListPath: 'full-path/-/issues', hasIssueWeightsFeature: false, + hasIterationsFeature: false, + hasOkrsFeature: false, + hasIssuableHealthStatusFeature: false, + }, + stubs: { + WorkItemWeight: true, + WorkItemIteration: true, }, }); }; |