diff options
Diffstat (limited to 'spec/frontend/work_items')
14 files changed, 753 insertions, 249 deletions
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 1b204b6fd60..7367212e49f 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -8,7 +8,7 @@ import { mockTracking } from 'helpers/tracking_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; -import { temporaryConfig } from '~/graphql_shared/issuable_client'; +import { config } from '~/graphql_shared/issuable_client'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; @@ -86,7 +86,7 @@ describe('WorkItemAssignees component', () => { ], {}, { - typePolicies: temporaryConfig.cacheConfig.typePolicies, + typePolicies: config.cacheConfig.typePolicies, }, ); 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 new file mode 100644 index 00000000000..01ab7824975 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js @@ -0,0 +1,108 @@ +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 { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data'; + +describe('WorkItemDescription', () => { + let wrapper; + + const findEditButton = () => wrapper.find('[data-testid="edit-description"]'); + const findCheckboxAtIndex = (index) => wrapper.findAll('input[type="checkbox"]').at(index); + + const defaultWorkItemDescription = { + description: descriptionTextWithCheckboxes, + descriptionHtml: descriptionHtmlWithCheckboxes, + }; + + const createComponent = ({ + workItemDescription = defaultWorkItemDescription, + canEdit = false, + } = {}) => { + wrapper = shallowMount(WorkItemDescriptionRendered, { + propsData: { + workItemDescription, + canEdit, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders gfm', async () => { + const renderGFMSpy = jest.spyOn($.fn, 'renderGFM'); + + createComponent(); + + await nextTick(); + + expect(renderGFMSpy).toHaveBeenCalled(); + }); + + describe('with checkboxes', () => { + beforeEach(() => { + createComponent({ + canEdit: true, + workItemDescription: { + description: `- [x] todo 1\n- [ ] todo 2`, + descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0"> +<li class="task-list-item" data-sourcepos="1:1-2:15"> +<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li> +<li class="task-list-item" data-sourcepos="2:1-2:15"> +<input class="task-list-item-checkbox" type="checkbox"> todo 2</li> +</ul>`, + }, + }); + }); + + it('checks unchecked checkbox', async () => { + findCheckboxAtIndex(1).setChecked(); + + await nextTick(); + + const updatedDescription = `- [x] todo 1\n- [x] todo 2`; + expect(wrapper.emitted('descriptionUpdated')).toEqual([[updatedDescription]]); + }); + + it('disables checkbox while updating', async () => { + findCheckboxAtIndex(1).setChecked(); + + await nextTick(); + + expect(findCheckboxAtIndex(1).attributes().disabled).toBeDefined(); + }); + + it('unchecks checked checkbox', async () => { + findCheckboxAtIndex(0).setChecked(false); + + await nextTick(); + + const updatedDescription = `- [ ] todo 1\n- [ ] todo 2`; + expect(wrapper.emitted('descriptionUpdated')).toEqual([[updatedDescription]]); + }); + }); + + describe('Edit button', () => { + it('is not visible when canUpdate = false', async () => { + await createComponent({ + canUpdate: false, + }); + + expect(findEditButton().exists()).toBe(false); + }); + + it('toggles edit mode', async () => { + createComponent({ + canEdit: true, + }); + + findEditButton().vm.$emit('click'); + + await nextTick(); + + expect(wrapper.emitted('startEditing')).toEqual([[]]); + }); + }); +}); 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 0691fe25e0d..c79b049442d 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -8,21 +8,23 @@ import EditedAt from '~/issues/show/components/edited.vue'; import { updateDraft } from '~/lib/utils/autosave'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; +import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { updateWorkItemMutationResponse, + workItemDescriptionSubscriptionResponse, workItemResponseFactory, workItemQueryResponse, + projectWorkItemResponse, } from '../mock_data'; -jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => { - return { - confirmAction: jest.fn(), - }; -}); +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); jest.mock('~/lib/utils/autosave'); const workItemId = workItemQueryResponse.data.workItem.id; @@ -33,12 +35,22 @@ describe('WorkItemDescription', () => { Vue.use(VueApollo); const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); + const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse); + const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); + let workItemResponseHandler; + let workItemsMvc2; - const findEditButton = () => wrapper.find('[data-testid="edit-description"]'); const findMarkdownField = () => wrapper.findComponent(MarkdownField); + const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); + const findRenderedDescription = () => wrapper.findComponent(WorkItemDescriptionRendered); const findEditedAt = () => wrapper.findComponent(EditedAt); - const editDescription = (newText) => wrapper.find('textarea').setValue(newText); + const editDescription = (newText) => { + if (workItemsMvc2) { + return findMarkdownEditor().vm.$emit('input', newText); + } + return wrapper.find('textarea').setValue(newText); + }; const clickCancel = () => wrapper.find('[data-testid="cancel"]').vm.$emit('click'); const clickSave = () => wrapper.find('[data-testid="save-description"]').vm.$emit('click', {}); @@ -48,18 +60,30 @@ describe('WorkItemDescription', () => { canUpdate = true, workItemResponse = workItemResponseFactory({ canUpdate }), isEditing = false, + fetchByIid = false, } = {}) => { - const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); const { id } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemDescription, { apolloProvider: createMockApollo([ [workItemQuery, workItemResponseHandler], [updateWorkItemMutation, mutationHandler], + [workItemDescriptionSubscription, subscriptionHandler], + [workItemByIidQuery, workItemByIidResponseHandler], ]), propsData: { workItemId: id, fullPath: 'test-project-path', + queryVariables: { + id: workItemId, + }, + fetchByIid, + }, + provide: { + glFeatures: { + workItemsMvc2, + }, }, stubs: { MarkdownField, @@ -69,7 +93,7 @@ describe('WorkItemDescription', () => { await waitForPromises(); if (isEditing) { - findEditButton().vm.$emit('click'); + findRenderedDescription().vm.$emit('startEditing'); await nextTick(); } @@ -79,171 +103,178 @@ describe('WorkItemDescription', () => { wrapper.destroy(); }); - describe('Edit button', () => { - it('is not visible when canUpdate = false', async () => { - await createComponent({ - canUpdate: false, + describe.each([true, false])( + 'editing description with workItemsMvc2 %workItemsMvc2Enabled', + (workItemsMvc2Enabled) => { + beforeEach(() => { + beforeEach(() => { + workItemsMvc2 = workItemsMvc2Enabled; + }); }); - expect(findEditButton().exists()).toBe(false); - }); + describe('editing description', () => { + it('shows edited by text', async () => { + const lastEditedAt = '2022-09-21T06:18:42Z'; + const lastEditedBy = { + name: 'Administrator', + webPath: '/root', + }; - it('toggles edit mode', async () => { - await createComponent({ - canUpdate: true, - }); + await createComponent({ + workItemResponse: workItemResponseFactory({ + lastEditedAt, + lastEditedBy, + }), + }); - findEditButton().vm.$emit('click'); + expect(findEditedAt().props()).toEqual({ + updatedAt: lastEditedAt, + updatedByName: lastEditedBy.name, + updatedByPath: lastEditedBy.webPath, + }); + }); - await nextTick(); + it('does not show edited by text', async () => { + await createComponent(); - expect(findMarkdownField().exists()).toBe(true); - }); - }); + expect(findEditedAt().exists()).toBe(false); + }); - describe('editing description', () => { - it('shows edited by text', async () => { - const lastEditedAt = '2022-09-21T06:18:42Z'; - const lastEditedBy = { - name: 'Administrator', - webPath: '/root', - }; - - await createComponent({ - workItemResponse: workItemResponseFactory({ - lastEditedAt, - lastEditedBy, - }), - }); + it('cancels when clicking cancel', async () => { + await createComponent({ + isEditing: true, + }); - expect(findEditedAt().props()).toEqual({ - updatedAt: lastEditedAt, - updatedByName: lastEditedBy.name, - updatedByPath: lastEditedBy.webPath, - }); - }); + clickCancel(); - it('does not show edited by text', async () => { - await createComponent(); + await nextTick(); - expect(findEditedAt().exists()).toBe(false); - }); + expect(confirmAction).not.toHaveBeenCalled(); + expect(findMarkdownField().exists()).toBe(false); + }); - it('cancels when clicking cancel', async () => { - await createComponent({ - isEditing: true, - }); + it('prompts for confirmation when clicking cancel after changes', async () => { + await createComponent({ + isEditing: true, + }); - clickCancel(); + editDescription('updated desc'); - await nextTick(); + clickCancel(); - expect(confirmAction).not.toHaveBeenCalled(); - expect(findMarkdownField().exists()).toBe(false); - }); + await nextTick(); - it('prompts for confirmation when clicking cancel after changes', async () => { - await createComponent({ - isEditing: true, - }); + expect(confirmAction).toHaveBeenCalled(); + }); - editDescription('updated desc'); + it('calls update widgets mutation', async () => { + const updatedDesc = 'updated desc'; - clickCancel(); + await createComponent({ + isEditing: true, + }); - await nextTick(); + editDescription(updatedDesc); - expect(confirmAction).toHaveBeenCalled(); - }); + clickSave(); - it('calls update widgets mutation', async () => { - await createComponent({ - isEditing: true, - }); + await waitForPromises(); - editDescription('updated desc'); + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + descriptionWidget: { + description: updatedDesc, + }, + }, + }); + }); - clickSave(); + it('tracks editing description', async () => { + await createComponent({ + isEditing: true, + markdownPreviewPath: '/preview', + }); + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - await waitForPromises(); + clickSave(); - expect(mutationSuccessHandler).toHaveBeenCalledWith({ - input: { - id: workItemId, - descriptionWidget: { - description: 'updated desc', - }, - }, - }); - }); + await waitForPromises(); - it('tracks editing description', async () => { - await createComponent({ - isEditing: true, - markdownPreviewPath: '/preview', - }); - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', { + category: TRACKING_CATEGORY_SHOW, + label: 'item_description', + property: 'type_Task', + }); + }); - clickSave(); + it('emits error when mutation returns error', async () => { + const error = 'eror'; - await waitForPromises(); + await createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockResolvedValue({ + data: { + workItemUpdate: { + workItem: {}, + errors: [error], + }, + }, + }), + }); - expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', { - category: TRACKING_CATEGORY_SHOW, - label: 'item_description', - property: 'type_Task', - }); - }); + editDescription('updated desc'); - it('emits error when mutation returns error', async () => { - const error = 'eror'; + clickSave(); - await createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockResolvedValue({ - data: { - workItemUpdate: { - workItem: {}, - errors: [error], - }, - }, - }), - }); + await waitForPromises(); - editDescription('updated desc'); + expect(wrapper.emitted('error')).toEqual([[error]]); + }); - clickSave(); + it('emits error when mutation fails', async () => { + const error = 'eror'; - await waitForPromises(); + await createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockRejectedValue(new Error(error)), + }); - expect(wrapper.emitted('error')).toEqual([[error]]); - }); + editDescription('updated desc'); - it('emits error when mutation fails', async () => { - const error = 'eror'; + clickSave(); - await createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockRejectedValue(new Error(error)), - }); + await waitForPromises(); - editDescription('updated desc'); + expect(wrapper.emitted('error')).toEqual([[error]]); + }); - clickSave(); + it('autosaves description', async () => { + await createComponent({ + isEditing: true, + }); - await waitForPromises(); + editDescription('updated desc'); - expect(wrapper.emitted('error')).toEqual([[error]]); - }); + expect(updateDraft).toHaveBeenCalled(); + }); + }); + + it('calls the global ID work item query when `fetchByIid` prop is false', async () => { + createComponent({ fetchByIid: false }); + await waitForPromises(); - it('autosaves description', async () => { - await createComponent({ - isEditing: true, + expect(workItemResponseHandler).toHaveBeenCalled(); + expect(workItemByIidResponseHandler).not.toHaveBeenCalled(); }); - editDescription('updated desc'); + it('calls the IID work item query when when `fetchByIid` prop is true', async () => { + createComponent({ fetchByIid: true }); + await waitForPromises(); - expect(updateDraft).toHaveBeenCalled(); - }); - }); + expect(workItemResponseHandler).not.toHaveBeenCalled(); + expect(workItemByIidResponseHandler).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 6b1ef8971d3..4029e47c390 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,6 +86,7 @@ describe('WorkItemDetailModal component', () => { isModal: true, workItemId: defaultPropsData.workItemId, workItemParentId: defaultPropsData.issueGid, + iid: 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 aae61b11196..26777b57797 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -24,12 +24,13 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; import WorkItemInformation from '~/work_items/components/work_item_information.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'; import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql'; import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; +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 { temporaryConfig } from '~/graphql_shared/issuable_client'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { mockParent, @@ -37,6 +38,8 @@ import { workItemResponseFactory, workItemTitleSubscriptionResponse, workItemAssigneesSubscriptionResponse, + workItemMilestoneSubscriptionResponse, + projectWorkItemResponse, } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -52,8 +55,12 @@ describe('WorkItemDetail component', () => { canDelete: true, }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const successByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const milestoneSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemMilestoneSubscriptionResponse); const assigneesSubscriptionHandler = jest .fn() .mockResolvedValue(workItemAssigneesSubscriptionResponse); @@ -85,26 +92,23 @@ describe('WorkItemDetail component', () => { subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], error = undefined, - includeWidgets = false, workItemsMvc2Enabled = false, + fetchByIid = false, + iidPathQueryParam = undefined, } = {}) => { const handlers = [ [workItemQuery, handler], [workItemTitleSubscription, subscriptionHandler], [workItemDatesSubscription, datesSubscriptionHandler], [workItemAssigneesSubscription, assigneesSubscriptionHandler], + [workItemMilestoneSubscription, milestoneSubscriptionHandler], + [workItemByIidQuery, successByIidHandler], confidentialityMock, ]; wrapper = shallowMount(WorkItemDetail, { - apolloProvider: createMockApollo( - handlers, - {}, - { - typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {}, - }, - ), - propsData: { isModal, workItemId }, + apolloProvider: createMockApollo(handlers), + propsData: { isModal, workItemId, iid: '1' }, data() { return { updateInProgress, @@ -114,15 +118,24 @@ describe('WorkItemDetail component', () => { provide: { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, + useIidInWorkItemsPath: fetchByIid, }, hasIssueWeightsFeature: true, hasIterationsFeature: true, projectNamespace: 'namespace', + fullPath: 'group/project', }, stubs: { WorkItemWeight: true, WorkItemIteration: true, }, + mocks: { + $route: { + query: { + iid_path: iidPathQueryParam, + }, + }, + }, }); }; @@ -421,8 +434,9 @@ describe('WorkItemDetail component', () => { }); describe('subscriptions', () => { - it('calls the title subscription', () => { + it('calls the title subscription', async () => { createComponent(); + await waitForPromises(); expect(titleSubscriptionHandler).toHaveBeenCalledWith({ issuableId: workItemQueryResponse.data.workItem.id, @@ -543,15 +557,41 @@ describe('WorkItemDetail component', () => { describe('milestone widget', () => { it.each` - description | includeWidgets | exists - ${'renders when widget is returned from API'} | ${true} | ${true} - ${'does not render when widget is not returned from API'} | ${false} | ${false} - `('$description', async ({ includeWidgets, exists }) => { - createComponent({ includeWidgets, workItemsMvc2Enabled: true }); + description | milestoneWidgetPresent | exists + ${'renders when widget is returned from API'} | ${true} | ${true} + ${'does not render when widget is not returned from API'} | ${false} | ${false} + `('$description', async ({ milestoneWidgetPresent, exists }) => { + const response = workItemResponseFactory({ milestoneWidgetPresent }); + const handler = jest.fn().mockResolvedValue(response); + createComponent({ handler, workItemsMvc2Enabled: true }); await waitForPromises(); expect(findWorkItemMilestone().exists()).toBe(exists); }); + + describe('milestone subscription', () => { + describe('when the milestone widget exists', () => { + it('calls the milestone subscription', async () => { + createComponent(); + await waitForPromises(); + + expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({ + issuableId: workItemQueryResponse.data.workItem.id, + }); + }); + }); + + describe('when the assignees widget does not exist', () => { + it('does not call the milestone subscription', async () => { + const response = workItemResponseFactory({ milestoneWidgetPresent: false }); + const handler = jest.fn().mockResolvedValue(response); + createComponent({ handler }); + await waitForPromises(); + + expect(milestoneSubscriptionHandler).not.toHaveBeenCalled(); + }); + }); + }); }); describe('work item information', () => { @@ -571,4 +611,35 @@ describe('WorkItemDetail component', () => { expect(findWorkItemInformationAlert().exists()).toBe(false); }); }); + + it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => { + createComponent(); + await waitForPromises(); + + expect(successHandler).toHaveBeenCalledWith({ + id: workItemQueryResponse.data.workItem.id, + }); + expect(successByIidHandler).not.toHaveBeenCalled(); + }); + + it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => { + createComponent({ fetchByIid: true }); + await waitForPromises(); + + expect(successHandler).toHaveBeenCalledWith({ + id: workItemQueryResponse.data.workItem.id, + }); + expect(successByIidHandler).not.toHaveBeenCalled(); + }); + + it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => { + createComponent({ fetchByIid: true, iidPathQueryParam: 'true' }); + await waitForPromises(); + + expect(successHandler).not.toHaveBeenCalled(); + expect(successByIidHandler).toHaveBeenCalledWith({ + fullPath: 'group/project', + iid: '1', + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js index 701406b9588..7ebaf8209c7 100644 --- a/spec/frontend/work_items/components/work_item_due_date_spec.js +++ b/spec/frontend/work_items/components/work_item_due_date_spec.js @@ -140,7 +140,7 @@ describe('WorkItemDueDate component', () => { beforeEach(() => { createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' }); - datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show'); + datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker, 'show'); findStartDatePicker().vm.$emit('input', startDate); findStartDatePicker().vm.$emit('close'); }); 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 e6ff7e8502d..9f7659b3f8d 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -9,6 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg 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'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants'; import { @@ -18,6 +19,7 @@ import { workItemResponseFactory, updateWorkItemMutationResponse, workItemLabelsSubscriptionResponse, + projectWorkItemResponse, } from '../mock_data'; Vue.use(VueApollo); @@ -33,6 +35,7 @@ describe('WorkItemLabels component', () => { const findLabelsTitle = () => wrapper.findByTestId('labels-title'); const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse); + const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse); const successUpdateWorkItemMutationHandler = jest .fn() @@ -45,12 +48,14 @@ describe('WorkItemLabels component', () => { workItemQueryHandler = workItemQuerySuccess, searchQueryHandler = successSearchQueryHandler, updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, + fetchByIid = false, } = {}) => { const apolloProvider = createMockApollo([ [workItemQuery, workItemQueryHandler], [labelSearchQuery, searchQueryHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], [workItemLabelsSubscription, subscriptionHandler], + [workItemByIidQuery, workItemByIidResponseHandler], ]); wrapper = mountExtended(WorkItemLabels, { @@ -58,6 +63,10 @@ describe('WorkItemLabels component', () => { workItemId, canUpdate, fullPath: 'test-project-path', + queryVariables: { + id: workItemId, + }, + fetchByIid, }, attachTo: document.body, apolloProvider, @@ -226,4 +235,20 @@ describe('WorkItemLabels component', () => { }); }); }); + + it('calls the global ID work item query when `fetchByIid` prop is false', async () => { + createComponent({ fetchByIid: false }); + await waitForPromises(); + + expect(workItemQuerySuccess).toHaveBeenCalled(); + expect(workItemByIidResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the IID work item query when when `fetchByIid` prop is true', async () => { + createComponent({ fetchByIid: true }); + await waitForPromises(); + + expect(workItemQuerySuccess).not.toHaveBeenCalled(); + expect(workItemByIidResponseHandler).toHaveBeenCalled(); + }); }); 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 ab3ea623e3e..071d5fb715a 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 @@ -1,10 +1,11 @@ import Vue from 'vue'; -import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui'; +import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui'; 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 WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; +import { FORM_TYPES } from '~/work_items/constants'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; @@ -14,6 +15,7 @@ import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse, updateWorkItemMutationResponse, + mockIterationWidgetResponse, } from '../../mock_data'; Vue.use(VueApollo); @@ -23,22 +25,35 @@ describe('WorkItemLinksForm', () => { const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse); + const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse); + + const mockParentIteration = mockIterationWidgetResponse; const createComponent = async ({ - listResponse = availableWorkItemsResponse, typesResponse = projectWorkItemTypesQueryResponse, parentConfidential = false, hasIterationsFeature = false, + workItemsMvc2Enabled = false, + parentIteration = null, + formType = FORM_TYPES.create, } = {}) => { wrapper = shallowMountExtended(WorkItemLinksForm, { apolloProvider: createMockApollo([ - [projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)], + [projectWorkItemsQuery, availableWorkItemsResolver], [projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)], [updateWorkItemMutation, updateMutationResolver], [createWorkItemMutation, createMutationResolver], ]), - propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential }, + propsData: { + issuableGid: 'gid://gitlab/WorkItem/1', + parentConfidential, + parentIteration, + formType, + }, provide: { + glFeatures: { + workItemsMvc2: workItemsMvc2Enabled, + }, projectPath: 'project/path', hasIterationsFeature, }, @@ -48,89 +63,155 @@ describe('WorkItemLinksForm', () => { }; const findForm = () => wrapper.findComponent(GlForm); - const findCombobox = () => wrapper.findComponent(GlFormCombobox); + const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); const findInput = () => wrapper.findComponent(GlFormInput); const findAddChildButton = () => wrapper.findByTestId('add-child-button'); - beforeEach(async () => { - await createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); - it('renders form', () => { - expect(findForm().exists()).toBe(true); - }); - - it('creates child task in non confidential parent', async () => { - findInput().vm.$emit('input', 'Create task test'); + describe('creating a new work item', () => { + beforeEach(async () => { + await createComponent(); + }); - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), + it('renders create form', () => { + expect(findForm().exists()).toBe(true); + expect(findInput().exists()).toBe(true); + expect(findAddChildButton().text()).toBe('Create task'); + expect(findTokenSelector().exists()).toBe(false); }); - await waitForPromises(); - expect(createMutationResolver).toHaveBeenCalledWith({ - input: { - title: 'Create task test', - projectPath: 'project/path', - workItemTypeId: 'gid://gitlab/WorkItems::Type/3', - hierarchyWidget: { - parentId: 'gid://gitlab/WorkItem/1', + + it('creates child task in non confidential parent', async () => { + findInput().vm.$emit('input', 'Create task test'); + + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalledWith({ + input: { + title: 'Create task test', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: false, }, - confidential: false, - }, + }); }); - }); - it('creates child task in confidential parent', async () => { - await createComponent({ parentConfidential: true }); + it('creates child task in confidential parent', async () => { + await createComponent({ parentConfidential: true }); - findInput().vm.$emit('input', 'Create confidential task'); + findInput().vm.$emit('input', 'Create confidential task'); - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); - await waitForPromises(); - expect(createMutationResolver).toHaveBeenCalledWith({ - input: { - title: 'Create confidential task', - projectPath: 'project/path', - workItemTypeId: 'gid://gitlab/WorkItems::Type/3', - hierarchyWidget: { - parentId: 'gid://gitlab/WorkItem/1', + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(createMutationResolver).toHaveBeenCalledWith({ + input: { + title: 'Create confidential task', + projectPath: 'project/path', + workItemTypeId: 'gid://gitlab/WorkItems::Type/3', + hierarchyWidget: { + parentId: 'gid://gitlab/WorkItem/1', + }, + confidential: true, }, - confidential: true, - }, + }); }); }); - // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('selects and add child', async () => { - findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]); + describe('adding an existing work item', () => { + beforeEach(async () => { + await createComponent({ formType: FORM_TYPES.add }); + }); - findAddChildButton().vm.$emit('click'); - await waitForPromises(); - expect(updateMutationResolver).toHaveBeenCalled(); - }); + it('renders add form', () => { + expect(findForm().exists()).toBe(true); + expect(findTokenSelector().exists()).toBe(true); + expect(findAddChildButton().text()).toBe('Add task'); + expect(findInput().exists()).toBe(false); + }); - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('when typing in combobox', () => { - beforeEach(async () => { - findCombobox().vm.$emit('input', 'Task'); + it('searches for available work items as prop when typing in input', async () => { + findTokenSelector().vm.$emit('focus'); + findTokenSelector().vm.$emit('text-input', 'Task'); await waitForPromises(); - await jest.runOnlyPendingTimers(); + + expect(availableWorkItemsResolver).toHaveBeenCalled(); }); - it('passes available work items as prop', () => { - expect(findCombobox().exists()).toBe(true); - expect(findCombobox().props('tokenList').length).toBe(2); + it('selects and adds children', async () => { + findTokenSelector().vm.$emit( + 'input', + availableWorkItemsResponse.data.workspace.workItems.nodes, + ); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + + await waitForPromises(); + + expect(findAddChildButton().text()).toBe('Add tasks'); + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(updateMutationResolver).toHaveBeenCalled(); }); + }); + + describe('associate iteration with task', () => { + it('does not update iteration when mvc2 feature flag is not enabled', async () => { + await createComponent({ + hasIterationsFeature: true, + parentIteration: mockParentIteration, + }); - it('passes action to create task', () => { - expect(findCombobox().props('actionList').length).toBe(1); + 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, + }); + findInput().vm.$emit('input', 'Create task test'); + + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(updateMutationResolver).toHaveBeenCalledWith({ + input: { + id: 'gid://gitlab/WorkItem/1', + iterationWidget: { + iterationId: mockParentIteration.id, + }, + }, + }); + }); + it('does not update when parent has no iteration associated', async () => { + await createComponent({ + workItemsMvc2Enabled: true, + hasIterationsFeature: true, + }); + findInput().vm.$emit('input', 'Create task test'); + + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + await waitForPromises(); + expect(updateMutationResolver).not.toHaveBeenCalled(); }); }); }); 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 6961996f912..66ce2c1becf 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 @@ -8,6 +8,7 @@ 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 { 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'; @@ -41,6 +42,13 @@ const issueDetailsResponse = (confidential = false) => ({ }, __typename: 'Iteration', }, + milestone: { + dueDate: null, + expired: false, + id: 'gid://gitlab/Milestone/28', + title: 'v2.0', + __typename: 'Milestone', + }, __typename: 'Issue', }, __typename: 'Project', @@ -107,7 +115,9 @@ describe('WorkItemLinks', () => { const findToggleButton = () => wrapper.findByTestId('toggle-links'); const findLinksBody = () => wrapper.findByTestId('links-body'); const findEmptyState = () => wrapper.findByTestId('links-empty'); + const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); + const findToggleCreateFormButton = () => wrapper.findByTestId('toggle-create-form'); const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0); const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); @@ -136,11 +146,27 @@ describe('WorkItemLinks', () => { }); describe('add link form', () => { - it('displays form on click add button and hides form on cancel', async () => { + it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => { + findToggleFormDropdown().vm.$emit('click'); findToggleAddFormButton().vm.$emit('click'); await nextTick(); expect(findAddLinksForm().exists()).toBe(true); + expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.add); + + findAddLinksForm().vm.$emit('cancel'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(false); + }); + + it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => { + findToggleFormDropdown().vm.$emit('click'); + findToggleCreateFormButton().vm.$emit('click'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(true); + expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.create); findAddLinksForm().vm.$emit('cancel'); await nextTick(); @@ -193,7 +219,7 @@ describe('WorkItemLinks', () => { }); it('does not display button to toggle Add form', () => { - expect(findToggleAddFormButton().exists()).toBe(false); + expect(findToggleFormDropdown().exists()).toBe(false); }); it('does not display link menu on children', () => { @@ -283,6 +309,7 @@ describe('WorkItemLinks', () => { await createComponent({ issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)), }); + findToggleFormDropdown().vm.$emit('click'); findToggleAddFormButton().vm.$emit('click'); await nextTick(); 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 08cdf62ae52..60ba2b55f76 100644 --- a/spec/frontend/work_items/components/work_item_milestone_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_spec.js @@ -9,7 +9,7 @@ import { import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; -import { resolvers, temporaryConfig } from '~/graphql_shared/issuable_client'; +import { resolvers, config } from '~/graphql_shared/issuable_client'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -22,8 +22,14 @@ import { mockMilestoneWidgetResponse, workItemResponseFactory, updateWorkItemMutationErrorResponse, + workItemMilestoneSubscriptionResponse, + projectWorkItemResponse, + updateWorkItemMutationResponse, } from 'jest/work_items/mock_data'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; +import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; describe('WorkItemMilestone component', () => { Vue.use(VueApollo); @@ -47,6 +53,8 @@ describe('WorkItemMilestone component', () => { const findInputGroup = () => wrapper.findComponent(GlFormGroup); const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true }); + const workItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); const networkResolvedValue = new Error(); @@ -54,6 +62,12 @@ describe('WorkItemMilestone component', () => { const successSearchWithNoMatchingMilestones = jest .fn() .mockResolvedValue(projectMilestonesResponseWithNoMilestones); + const milestoneSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemMilestoneSubscriptionResponse); + const successUpdateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponse); const showDropdown = () => { findDropdown().vm.$emit('shown'); @@ -67,12 +81,20 @@ describe('WorkItemMilestone component', () => { canUpdate = true, milestone = mockMilestoneWidgetResponse, searchQueryHandler = successSearchQueryHandler, + fetchByIid = false, + mutationHandler = successUpdateWorkItemMutationHandler, } = {}) => { const apolloProvider = createMockApollo( - [[projectMilestonesQuery, searchQueryHandler]], + [ + [workItemQuery, workItemQueryHandler], + [workItemMilestoneSubscription, milestoneSubscriptionHandler], + [projectMilestonesQuery, searchQueryHandler], + [updateWorkItemMutation, mutationHandler], + [workItemByIidQuery, workItemByIidResponseHandler], + ], resolvers, { - typePolicies: temporaryConfig.cacheConfig.typePolicies, + typePolicies: config.cacheConfig.typePolicies, }, ); @@ -92,6 +114,10 @@ describe('WorkItemMilestone component', () => { workItemId, workItemType, fullPath, + queryVariables: { + id: workItemId, + }, + fetchByIid, }, stubs: { GlDropdown, diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index ed90b11222a..635a1f326f8 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -41,6 +41,7 @@ export const workItemQueryResponse = { workItem: { __typename: 'WorkItem', id: 'gid://gitlab/WorkItem/1', + iid: '1', title: 'Test', state: 'OPEN', description: 'description', @@ -113,6 +114,7 @@ export const updateWorkItemMutationResponse = { workItem: { __typename: 'WorkItem', id: 'gid://gitlab/WorkItem/1', + iid: '1', title: 'Updated title', state: 'OPEN', description: 'description', @@ -178,6 +180,19 @@ export const mockParent = { }, }; +export const descriptionTextWithCheckboxes = `- [ ] todo 1\n- [ ] todo 2`; + +export const descriptionHtmlWithCheckboxes = ` + <ul dir="auto" class="task-list" data-sourcepos"1:1-2:12"> + <li class="task-list-item" data-sourcepos="1:1-1:11"> + <input class="task-list-item-checkbox" type="checkbox"> todo 1 + </li> + <li class="task-list-item" data-sourcepos="2:1-2:12"> + <input class="task-list-item-checkbox" type="checkbox"> todo 2 + </li> + </ul> +`; + export const workItemResponseFactory = ({ canUpdate = false, canDelete = false, @@ -193,12 +208,14 @@ export const workItemResponseFactory = ({ allowsScopedLabels = false, lastEditedAt = null, lastEditedBy = null, + withCheckboxes = false, parent = mockParent.parent, } = {}) => ({ data: { workItem: { __typename: 'WorkItem', id: 'gid://gitlab/WorkItem/1', + iid: 1, title: 'Updated title', state: 'OPEN', description: 'description', @@ -224,9 +241,10 @@ export const workItemResponseFactory = ({ { __typename: 'WorkItemWidgetDescription', type: 'DESCRIPTION', - description: 'some **great** text', - descriptionHtml: - '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>', + description: withCheckboxes ? descriptionTextWithCheckboxes : 'some **great** text', + descriptionHtml: withCheckboxes + ? descriptionHtmlWithCheckboxes + : '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>', lastEditedAt, lastEditedBy, }, @@ -283,11 +301,12 @@ export const workItemResponseFactory = ({ milestoneWidgetPresent ? { __typename: 'WorkItemWidgetMilestone', - dueDate: null, - expired: false, - id: 'gid://gitlab/Milestone/30', - title: 'v4.0', type: 'MILESTONE', + milestone: { + expired: false, + id: 'gid://gitlab/Milestone/30', + title: 'v4.0', + }, } : { type: 'MOCK TYPE' }, { @@ -312,7 +331,8 @@ export const workItemResponseFactory = ({ export const projectWorkItemTypesQueryResponse = { data: { workspace: { - id: 'gid://gitlab/WorkItem/1', + __typename: 'Project', + id: 'gid://gitlab/Project/2', workItemTypes: { nodes: [ { id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' }, @@ -331,6 +351,7 @@ export const createWorkItemMutationResponse = { workItem: { __typename: 'WorkItem', id: 'gid://gitlab/WorkItem/1', + iid: '1', title: 'Updated title', state: 'OPEN', description: 'description', @@ -368,6 +389,7 @@ export const createWorkItemFromTaskMutationResponse = { __typename: 'WorkItem', description: 'New description', id: 'gid://gitlab/WorkItem/1', + iid: '1', title: 'Updated title', state: 'OPEN', confidential: false, @@ -405,6 +427,7 @@ export const createWorkItemFromTaskMutationResponse = { newWorkItem: { __typename: 'WorkItem', id: 'gid://gitlab/WorkItem/1000000', + iid: '100', title: 'Updated title', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', @@ -498,6 +521,28 @@ export const workItemTitleSubscriptionResponse = { }, }; +export const workItemDescriptionSubscriptionResponse = { + data: { + issuableDescriptionUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetDescription', + type: 'DESCRIPTION', + description: 'New description', + descriptionHtml: '<p>New description</p>', + lastEditedAt: '2022-09-21T06:18:42Z', + lastEditedBy: { + id: 'gid://gitlab/User/2', + name: 'Someone else', + webPath: '/not-you', + }, + }, + ], + }, + }, +}; + export const workItemWeightSubscriptionResponse = { data: { issuableWeightUpdated: { @@ -567,6 +612,25 @@ export const workItemIterationSubscriptionResponse = { }, }; +export const workItemMilestoneSubscriptionResponse = { + data: { + issuableMilestoneUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetMilestone', + type: 'MILESTONE', + milestone: { + id: 'gid://gitlab/Milestone/1125', + expired: false, + title: 'Milestone title', + }, + }, + ], + }, + }, +}; + export const workItemHierarchyEmptyResponse = { data: { workItem: { @@ -776,6 +840,7 @@ export const changeWorkItemParentMutationResponse = { }, description: null, id: 'gid://gitlab/WorkItem/2', + iid: '2', state: 'OPEN', title: 'Foo', confidential: false, @@ -809,22 +874,20 @@ export const availableWorkItemsResponse = { __typename: 'Project', id: 'gid://gitlab/Project/2', workItems: { - edges: [ + nodes: [ { - node: { - id: 'gid://gitlab/WorkItem/458', - title: 'Task 1', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - }, + id: 'gid://gitlab/WorkItem/458', + title: 'Task 1', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + __typename: 'WorkItem', }, { - node: { - id: 'gid://gitlab/WorkItem/459', - title: 'Task 2', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - }, + id: 'gid://gitlab/WorkItem/459', + title: 'Task 2', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + __typename: 'WorkItem', }, ], }, @@ -1072,7 +1135,7 @@ export const groupIterationsResponseWithNoIterations = { }; export const mockMilestoneWidgetResponse = { - dueDate: null, + state: 'active', expired: false, id: 'gid://gitlab/Milestone/30', title: 'v4.0', @@ -1122,3 +1185,14 @@ export const projectMilestonesResponseWithNoMilestones = { }, }, }; + +export const projectWorkItemResponse = { + data: { + workspace: { + id: 'gid://gitlab/Project/1', + workItems: { + nodes: [workItemQueryResponse.data.workItem], + }, + }, + }, +}; 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 15dac25b7d9..387c8a355fa 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -37,12 +37,17 @@ describe('Create work item component', () => { props = {}, queryHandler = querySuccessHandler, mutationHandler = createWorkItemSuccessHandler, + fetchByIid = false, } = {}) => { - fakeApollo = createMockApollo([ - [projectWorkItemTypesQuery, queryHandler], - [createWorkItemMutation, mutationHandler], - [createWorkItemFromTaskMutation, mutationHandler], - ]); + fakeApollo = createMockApollo( + [ + [projectWorkItemTypesQuery, queryHandler], + [createWorkItemMutation, mutationHandler], + [createWorkItemFromTaskMutation, mutationHandler], + ], + {}, + { typePolicies: { Project: { merge: true } } }, + ); wrapper = shallowMount(CreateWorkItem, { apolloProvider: fakeApollo, data() { @@ -61,6 +66,9 @@ describe('Create work item component', () => { }, provide: { fullPath: 'full-path', + glFeatures: { + useIidInWorkItemsPath: fetchByIid, + }, }, }); }; @@ -99,7 +107,12 @@ describe('Create work item component', () => { wrapper.find('form').trigger('submit'); await waitForPromises(); - expect(wrapper.vm.$router.push).toHaveBeenCalled(); + expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + name: 'workItem', + params: { + id: '1', + }, + }); }); it('adds right margin for create button', () => { @@ -197,4 +210,18 @@ describe('Create work item component', () => { 'Something went wrong when creating work item. Please try again.', ); }); + + it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => { + createComponent({ fetchByIid: true }); + findTitleInput().vm.$emit('title-input', 'Test title'); + + wrapper.find('form').trigger('submit'); + await waitForPromises(); + + expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + name: 'workItem', + params: { id: '1' }, + query: { iid_path: '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 d9372f2bcf0..880c4271024 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -55,6 +55,7 @@ describe('Work items root component', () => { isModal: false, workItemId: 'gid://gitlab/WorkItem/1', workItemParentId: null, + iid: '1', }); }); @@ -65,11 +66,15 @@ describe('Work items root component', () => { deleteWorkItemHandler, }); - findWorkItemDetail().vm.$emit('deleteWorkItem'); + findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' }); await waitForPromises(); - expect(deleteWorkItemHandler).toHaveBeenCalled(); + expect(deleteWorkItemHandler).toHaveBeenCalledWith({ + input: { + id: '1', + }, + }); expect(mockToastShow).toHaveBeenCalled(); expect(visitUrl).toHaveBeenCalledWith(issuesListPath); }); @@ -81,7 +86,7 @@ describe('Work items root component', () => { deleteWorkItemHandler, }); - findWorkItemDetail().vm.$emit('deleteWorkItem'); + findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' }); await waitForPromises(); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 66a917d8052..982f9f71f9e 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -10,6 +10,8 @@ import { workItemTitleSubscriptionResponse, workItemWeightSubscriptionResponse, workItemLabelsSubscriptionResponse, + workItemMilestoneSubscriptionResponse, + workItemDescriptionSubscriptionResponse, } from 'jest/work_items/mock_data'; import App from '~/work_items/components/app.vue'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; @@ -17,6 +19,8 @@ import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subs import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql'; +import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql'; +import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql'; 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'; @@ -34,6 +38,12 @@ describe('Work items router', () => { .fn() .mockResolvedValue(workItemAssigneesSubscriptionResponse); const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse); + const milestoneSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemMilestoneSubscriptionResponse); + const descriptionSubscriptionHandler = jest + .fn() + .mockResolvedValue(workItemDescriptionSubscriptionResponse); const createComponent = async (routeArg) => { const router = createRouter('/work_item'); @@ -47,6 +57,8 @@ describe('Work items router', () => { [workItemTitleSubscription, titleSubscriptionHandler], [workItemAssigneesSubscription, assigneesSubscriptionHandler], [workItemLabelsSubscription, labelsSubscriptionHandler], + [workItemMilestoneSubscription, milestoneSubscriptionHandler], + [workItemDescriptionSubscription, descriptionSubscriptionHandler], ]; if (IS_EE) { @@ -59,10 +71,19 @@ describe('Work items router', () => { provide: { fullPath: 'full-path', issuesListPath: 'full-path/-/issues', + hasIssueWeightsFeature: false, }, }); }; + beforeEach(() => { + window.gon = { + features: { + workItemsMvc2: false, + }, + }; + }); + afterEach(() => { wrapper.destroy(); window.location.hash = ''; @@ -74,7 +95,14 @@ describe('Work items router', () => { expect(wrapper.findComponent(WorkItemsRoot).exists()).toBe(true); }); + it('does not render create work item page on `/new` route if `workItemsMvc2` feature flag is off', async () => { + await createComponent('/new'); + + expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(false); + }); + it('renders create work item page on `/new` route', async () => { + window.gon.features.workItemsMvc2 = true; await createComponent('/new'); expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(true); |