diff options
Diffstat (limited to 'spec/frontend/work_items')
18 files changed, 1002 insertions, 409 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index de20369eb1b..13e04ef6671 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -49,6 +49,6 @@ describe('ItemTitle', () => { findInputEl().element.innerText = mockUpdatedTitle; await findInputEl().trigger(sourceEvent); - expect(wrapper.emitted(eventName)).toBeTruthy(); + expect(wrapper.emitted(eventName)).toBeDefined(); }); }); diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js index a1f1d47ab90..3c312fb4552 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -1,15 +1,30 @@ -import { GlModal } from '@gitlab/ui'; +import { GlDropdownDivider, GlModal } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; +const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action'; +const TEST_ID_DELETE_ACTION = 'delete-action'; + describe('WorkItemActions component', () => { let wrapper; let glModalDirective; const findModal = () => wrapper.findComponent(GlModal); const findConfidentialityToggleButton = () => - wrapper.findByTestId('confidentiality-toggle-action'); - const findDeleteButton = () => wrapper.findByTestId('delete-action'); + wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION); + const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION); + const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *'); + const findDropdownItemsActual = () => + findDropdownItems().wrappers.map((x) => { + if (x.is(GlDropdownDivider)) { + return { divider: true }; + } + + return { + testId: x.attributes('data-testid'), + text: x.text(), + }; + }); const createComponent = ({ canUpdate = true, @@ -19,7 +34,14 @@ describe('WorkItemActions component', () => { } = {}) => { glModalDirective = jest.fn(); wrapper = shallowMountExtended(WorkItemActions, { - propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential }, + propsData: { + workItemId: '123', + canUpdate, + canDelete, + isConfidential, + isParentConfidential, + workItemType: 'Task', + }, directives: { glModal: { bind(_, { value }) { @@ -44,8 +66,19 @@ describe('WorkItemActions component', () => { it('renders dropdown actions', () => { createComponent(); - expect(findConfidentialityToggleButton().exists()).toBe(true); - expect(findDeleteButton().exists()).toBe(true); + expect(findDropdownItemsActual()).toEqual([ + { + testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, + text: 'Turn on confidentiality', + }, + { + divider: true, + }, + { + testId: TEST_ID_DELETE_ACTION, + text: 'Delete task', + }, + ]); }); describe('toggle confidentiality action', () => { @@ -103,7 +136,8 @@ describe('WorkItemActions component', () => { canDelete: false, }); - expect(wrapper.findByTestId('delete-action').exists()).toBe(false); + expect(findDeleteButton().exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false); }); }); }); 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 f0ef8aee7a9..28231fad108 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -1,4 +1,4 @@ -import { GlLink, GlTokenSelector, GlSkeletonLoader } from '@gitlab/ui'; +import { GlLink, GlTokenSelector, GlSkeletonLoader, GlIntersectionObserver } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -8,12 +8,17 @@ 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 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'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; -import { i18n, TASK_TYPE_NAME, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import { temporaryConfig } from '~/work_items/graphql/provider'; +import { + i18n, + TASK_TYPE_NAME, + TRACKING_CATEGORY_SHOW, + DEFAULT_PAGE_SIZE_ASSIGNEES, +} from '~/work_items/constants'; import { projectMembersResponseWithCurrentUser, mockAssignees, @@ -22,6 +27,8 @@ import { currentUserNullResponse, projectMembersResponseWithoutCurrentUser, updateWorkItemMutationResponse, + projectMembersResponseWithCurrentUserWithNextPage, + projectMembersResponseWithNoMatchingUsers, } from '../mock_data'; Vue.use(VueApollo); @@ -40,15 +47,25 @@ describe('WorkItemAssignees component', () => { const findEmptyState = () => wrapper.findByTestId('empty-state'); const findAssignSelfButton = () => wrapper.findByTestId('assign-self'); const findAssigneesTitle = () => wrapper.findByTestId('assignees-title'); + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + + const triggerInfiniteScroll = () => + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); const successSearchQueryHandler = jest .fn() .mockResolvedValue(projectMembersResponseWithCurrentUser); + const successSearchQueryHandlerWithMoreAssignees = jest + .fn() + .mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage); const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse); const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse); const successUpdateWorkItemMutationHandler = jest .fn() .mockResolvedValue(updateWorkItemMutationResponse); + const successSearchWithNoMatchingUsers = jest + .fn() + .mockResolvedValue(projectMembersResponseWithNoMatchingUsers); const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); @@ -82,9 +99,6 @@ describe('WorkItemAssignees component', () => { }); wrapper = mountExtended(WorkItemAssignees, { - provide: { - fullPath: 'test-project-path', - }, propsData: { assignees, workItemId, @@ -92,6 +106,7 @@ describe('WorkItemAssignees component', () => { workItemType: TASK_TYPE_NAME, canUpdate, canInviteMembers, + fullPath: 'test-project-path', }, attachTo: document.body, apolloProvider, @@ -459,4 +474,56 @@ describe('WorkItemAssignees component', () => { expect(findInviteMembersTrigger().exists()).toBe(true); }); }); + + describe('load more assignees', () => { + it('does not have intersection observer when no matching users', async () => { + createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers }); + findTokenSelector().vm.$emit('focus'); + await nextTick(); + + expect(findSkeletonLoader().exists()).toBe(true); + + await waitForPromises(); + + expect(findSkeletonLoader().exists()).toBe(false); + expect(findIntersectionObserver().exists()).toBe(false); + }); + + it('does not trigger load more when does not have next page', async () => { + createComponent(); + findTokenSelector().vm.$emit('focus'); + await nextTick(); + + expect(findSkeletonLoader().exists()).toBe(true); + + await waitForPromises(); + + expect(findSkeletonLoader().exists()).toBe(false); + + expect(findIntersectionObserver().exists()).toBe(false); + }); + + it('triggers load more when there are more users', async () => { + createComponent({ searchQueryHandler: successSearchQueryHandlerWithMoreAssignees }); + findTokenSelector().vm.$emit('focus'); + await nextTick(); + + expect(findSkeletonLoader().exists()).toBe(true); + + await waitForPromises(); + + expect(findSkeletonLoader().exists()).toBe(false); + expect(findIntersectionObserver().exists()).toBe(true); + + triggerInfiniteScroll(); + + expect(successSearchQueryHandlerWithMoreAssignees).toHaveBeenCalledWith({ + first: DEFAULT_PAGE_SIZE_ASSIGNEES, + after: + projectMembersResponseWithCurrentUserWithNextPage.data.workspace.users.pageInfo.endCursor, + search: '', + fullPath: 'test-project-path', + }); + }); + }); }); 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 8017c46dea8..d3165d8dc26 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -10,9 +10,9 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; -import updateWorkItemWidgetsMutation from '~/work_items/graphql/update_work_item_widgets.mutation.graphql'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import { - updateWorkItemWidgetsResponse, + updateWorkItemMutationResponse, workItemResponseFactory, workItemQueryResponse, } from '../mock_data'; @@ -31,7 +31,7 @@ describe('WorkItemDescription', () => { Vue.use(VueApollo); - const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse); + const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); const findEditButton = () => wrapper.find('[data-testid="edit-description"]'); const findMarkdownField = () => wrapper.findComponent(MarkdownField); @@ -53,13 +53,11 @@ describe('WorkItemDescription', () => { wrapper = shallowMount(WorkItemDescription, { apolloProvider: createMockApollo([ [workItemQuery, workItemResponseHandler], - [updateWorkItemWidgetsMutation, mutationHandler], + [updateWorkItemMutation, mutationHandler], ]), propsData: { workItemId: id, - }, - provide: { - fullPath: '/group/project', + fullPath: 'test-project-path', }, stubs: { MarkdownField, @@ -175,7 +173,7 @@ describe('WorkItemDescription', () => { isEditing: true, mutationHandler: jest.fn().mockResolvedValue({ data: { - workItemUpdateWidgets: { + workItemUpdate: { workItem: {}, errors: [error], }, 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 01891012f99..6b1ef8971d3 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 @@ -113,7 +113,7 @@ describe('WorkItemDetailModal component', () => { createComponent(); findModal().vm.$emit('hide'); - expect(wrapper.emitted('close')).toBeTruthy(); + expect(wrapper.emitted('close')).toHaveLength(1); }); it('hides the modal when WorkItemDetail emits `close` event', () => { diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 823981df880..b047e0dc8d7 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -2,29 +2,33 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gi import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } 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 waitForPromises from 'helpers/wait_for_promises'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; 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'; +import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue'; import WorkItemState from '~/work_items/components/work_item_state.vue'; 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 WorkItemWeight from '~/work_items/components/work_item_weight.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 workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql'; import workItemTitleSubscription from '~/work_items/graphql/work_item_title.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 '~/work_items/graphql/provider'; +import { temporaryConfig } from '~/graphql_shared/issuable_client'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { - workItemTitleSubscriptionResponse, - workItemResponseFactory, mockParent, + workItemDatesSubscriptionResponse, + workItemResponseFactory, + workItemTitleSubscriptionResponse, + workItemWeightSubscriptionResponse, } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -40,7 +44,9 @@ describe('WorkItemDetail component', () => { canDelete: true, }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); - const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); + const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); const findAlert = () => wrapper.findComponent(GlAlert); const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); @@ -49,9 +55,9 @@ describe('WorkItemDetail component', () => { const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); const findWorkItemState = () => wrapper.findComponent(WorkItemState); const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription); + const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate); const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); - const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight); const findParent = () => wrapper.find('[data-testid="work-item-parent"]'); const findParentButton = () => findParent().findComponent(GlButton); const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); @@ -64,19 +70,26 @@ describe('WorkItemDetail component', () => { updateInProgress = false, workItemId = workItemQueryResponse.data.workItem.id, handler = successHandler, - subscriptionHandler = initialSubscriptionHandler, + subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], workItemsMvc2Enabled = false, includeWidgets = false, error = undefined, } = {}) => { + const handlers = [ + [workItemQuery, handler], + [workItemTitleSubscription, subscriptionHandler], + [workItemDatesSubscription, datesSubscriptionHandler], + confidentialityMock, + ]; + + if (IS_EE) { + handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); + } + wrapper = shallowMount(WorkItemDetail, { apolloProvider: createMockApollo( - [ - [workItemQuery, handler], - [workItemTitleSubscription, subscriptionHandler], - confidentialityMock, - ], + handlers, {}, { typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {}, @@ -93,6 +106,7 @@ describe('WorkItemDetail component', () => { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, }, + hasIssueWeightsFeature: true, }, }); }; @@ -134,6 +148,10 @@ describe('WorkItemDetail component', () => { expect(findWorkItemState().exists()).toBe(true); expect(findWorkItemTitle().exists()).toBe(true); }); + + it('updates the document title', () => { + expect(document.title).toEqual('Updated title · Task · test-project-path'); + }); }); describe('close button', () => { @@ -295,8 +313,7 @@ describe('WorkItemDetail component', () => { await waitForPromises(); findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true); await waitForPromises(); - - expect(wrapper.emitted('workItemUpdated')).toBeFalsy(); + expect(wrapper.emitted('workItemUpdated')).toBeUndefined(); await nextTick(); @@ -379,23 +396,50 @@ describe('WorkItemDetail component', () => { it('shows an error message when WorkItemTitle emits an `error` event', async () => { createComponent(); await waitForPromises(); + const updateError = 'Failed to update'; - findWorkItemTitle().vm.$emit('error', i18n.updateError); + findWorkItemTitle().vm.$emit('error', updateError); await waitForPromises(); - expect(findAlert().text()).toBe(i18n.updateError); + expect(findAlert().text()).toBe(updateError); }); - it('calls the subscription', () => { - createComponent(); + describe('subscriptions', () => { + it('calls the title subscription', () => { + createComponent(); + + expect(titleSubscriptionHandler).toHaveBeenCalledWith({ + issuableId: workItemQueryResponse.data.workItem.id, + }); + }); - expect(initialSubscriptionHandler).toHaveBeenCalledWith({ - issuableId: workItemQueryResponse.data.workItem.id, + describe('dates subscription', () => { + describe('when the due date widget exists', () => { + it('calls the dates subscription', async () => { + createComponent(); + await waitForPromises(); + + expect(datesSubscriptionHandler).toHaveBeenCalledWith({ + issuableId: workItemQueryResponse.data.workItem.id, + }); + }); + }); + + describe('when the due date widget does not exist', () => { + it('does not call the dates subscription', async () => { + const response = workItemResponseFactory({ datesWidgetPresent: false }); + const handler = jest.fn().mockResolvedValue(response); + createComponent({ handler, workItemsMvc2Enabled: true }); + await waitForPromises(); + + expect(datesSubscriptionHandler).not.toHaveBeenCalled(); + }); + }); }); }); - describe('when work_items_mvc_2 feature flag is enabled', () => { - it('renders assignees component when assignees widget is returned from the API', async () => { + describe('assignees widget', () => { + it('renders assignees component when widget is returned from the API', async () => { createComponent({ workItemsMvc2Enabled: true, }); @@ -404,7 +448,7 @@ describe('WorkItemDetail component', () => { expect(findWorkItemAssignees().exists()).toBe(true); }); - it('does not render assignees component when assignees widget is not returned from the API', async () => { + it('does not render assignees component when widget is not returned from the API', async () => { createComponent({ workItemsMvc2Enabled: true, handler: jest @@ -417,13 +461,6 @@ describe('WorkItemDetail component', () => { }); }); - it('does not render assignees component when assignees feature flag is disabled', async () => { - createComponent(); - await waitForPromises(); - - expect(findWorkItemAssignees().exists()).toBe(false); - }); - describe('labels widget', () => { it.each` description | includeWidgets | exists @@ -437,30 +474,31 @@ describe('WorkItemDetail component', () => { }); }); - describe('weight widget', () => { + describe('dates widget', () => { describe.each` - description | weightWidgetPresent | exists - ${'when widget is returned from API'} | ${true} | ${true} - ${'when widget is not returned from API'} | ${false} | ${false} - `('$description', ({ weightWidgetPresent, exists }) => { - it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => { - const response = workItemResponseFactory({ weightWidgetPresent }); + description | datesWidgetPresent | exists + ${'when widget is returned from API'} | ${true} | ${true} + ${'when widget is not returned from API'} | ${false} | ${false} + `('$description', ({ datesWidgetPresent, exists }) => { + it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => { + const response = workItemResponseFactory({ datesWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler }); + createComponent({ handler, workItemsMvc2Enabled: true }); await waitForPromises(); - expect(findWorkItemWeight().exists()).toBe(exists); + expect(findWorkItemDueDate().exists()).toBe(exists); }); }); it('shows an error message when it emits an `error` event', async () => { createComponent({ workItemsMvc2Enabled: true }); await waitForPromises(); + const updateError = 'Failed to update'; - findWorkItemWeight().vm.$emit('error', i18n.updateError); + findWorkItemDueDate().vm.$emit('error', updateError); await waitForPromises(); - expect(findAlert().text()).toBe(i18n.updateError); + expect(findAlert().text()).toBe(updateError); }); }); 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 new file mode 100644 index 00000000000..1d76154a1f0 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_due_date_spec.js @@ -0,0 +1,346 @@ +import { GlFormGroup, GlDatepicker } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mockTracking } from 'helpers/tracking_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import { updateWorkItemMutationResponse, updateWorkItemMutationErrorResponse } from '../mock_data'; + +describe('WorkItemDueDate component', () => { + let wrapper; + + Vue.use(VueApollo); + + const workItemId = 'gid://gitlab/WorkItem/1'; + const updateWorkItemMutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); + + const findStartDateButton = () => + wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addStartDate }); + const findStartDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.startDate); + const findStartDatePicker = () => wrapper.findComponent(GlDatepicker); + const findDueDateButton = () => + wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addDueDate }); + const findDueDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.dueDate); + const findDueDatePicker = () => wrapper.findAllComponents(GlDatepicker).at(1); + const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); + + const createComponent = ({ + canUpdate = false, + dueDate = null, + startDate = null, + mutationHandler = updateWorkItemMutationHandler, + } = {}) => { + wrapper = mountExtended(WorkItemDueDate, { + apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), + propsData: { + canUpdate, + dueDate, + startDate, + workItemId, + workItemType: 'Task', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when can update', () => { + describe('start date', () => { + describe('`Add start date` button', () => { + describe.each` + description | startDate | exists + ${'when there is no start date'} | ${null} | ${true} + ${'when there is a start date'} | ${'2022-01-01'} | ${false} + `('$description', ({ startDate, exists }) => { + beforeEach(() => { + createComponent({ canUpdate: true, startDate }); + }); + + it(exists ? 'renders' : 'does not render', () => { + expect(findStartDateButton().exists()).toBe(exists); + }); + }); + + describe('when it emits `click` event', () => { + beforeEach(() => { + createComponent({ canUpdate: true, startDate: null }); + findStartDateButton().vm.$emit('click'); + }); + + it('renders start date picker', () => { + expect(findStartDateInput().exists()).toBe(true); + }); + + it('hides itself', () => { + expect(findStartDateButton().exists()).toBe(false); + }); + }); + }); + + describe('date picker', () => { + describe('when it emits a `clear` event', () => { + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' }); + findStartDatePicker().vm.$emit('clear'); + }); + + it('hides the date picker', () => { + expect(findStartDateInput().exists()).toBe(false); + }); + + it('shows the `Add start date` button', () => { + expect(findStartDateButton().exists()).toBe(true); + }); + + it('calls a mutation to update the dates', () => { + expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + startAndDueDateWidget: { + dueDate: new Date('2022-01-01T00:00:00.000Z'), + startDate: null, + }, + }, + }); + }); + }); + + describe('when it emits a `close` event', () => { + describe('when the start date is earlier than the due date', () => { + const startDate = new Date('2022-01-01T00:00:00.000Z'); + + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' }); + findStartDatePicker().vm.$emit('input', startDate); + findStartDatePicker().vm.$emit('close'); + }); + + it('calls a mutation to update the dates', () => { + expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + startAndDueDateWidget: { + dueDate: new Date('2022-12-31T00:00:00.000Z'), + startDate, + }, + }, + }); + }); + }); + + describe('when the start date is later than the due date', () => { + const startDate = new Date('2030-01-01T00:00:00.000Z'); + let datePickerOpenSpy; + + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' }); + datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show'); + findStartDatePicker().vm.$emit('input', startDate); + findStartDatePicker().vm.$emit('close'); + }); + + it('does not call a mutation to update the dates', () => { + expect(updateWorkItemMutationHandler).not.toHaveBeenCalled(); + }); + + it('updates the due date picker to the same date', () => { + expect(findDueDatePicker().props('value')).toEqual(startDate); + }); + + it('opens the due date picker', () => { + expect(datePickerOpenSpy).toHaveBeenCalled(); + }); + }); + }); + }); + }); + + describe('due date', () => { + describe('`Add due date` button', () => { + describe.each` + description | dueDate | exists + ${'when there is no due date'} | ${null} | ${true} + ${'when there is a due date'} | ${'2022-01-01'} | ${false} + `('$description', ({ dueDate, exists }) => { + beforeEach(() => { + createComponent({ canUpdate: true, dueDate }); + }); + + it(exists ? 'renders' : 'does not render', () => { + expect(findDueDateButton().exists()).toBe(exists); + }); + }); + + describe('when it emits `click` event', () => { + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: null }); + findDueDateButton().vm.$emit('click'); + }); + + it('renders due date picker', () => { + expect(findDueDateInput().exists()).toBe(true); + }); + + it('hides itself', () => { + expect(findDueDateButton().exists()).toBe(false); + }); + }); + }); + + describe('date picker', () => { + describe('when it emits a `clear` event', () => { + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' }); + findDueDatePicker().vm.$emit('clear'); + }); + + it('hides the date picker', () => { + expect(findDueDateInput().exists()).toBe(false); + }); + + it('shows the `Add due date` button', () => { + expect(findDueDateButton().exists()).toBe(true); + }); + + it('calls a mutation to update the dates', () => { + expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + startAndDueDateWidget: { + dueDate: null, + startDate: new Date('2022-01-01T00:00:00.000Z'), + }, + }, + }); + }); + }); + + describe('when it emits a `close` event', () => { + const dueDate = new Date('2022-12-31T00:00:00.000Z'); + + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' }); + findDueDatePicker().vm.$emit('input', dueDate); + findDueDatePicker().vm.$emit('close'); + }); + + it('calls a mutation to update the dates', () => { + expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + startAndDueDateWidget: { + dueDate, + startDate: new Date('2022-01-01T00:00:00.000Z'), + }, + }, + }); + }); + }); + }); + }); + + describe('when updating date', () => { + describe('when dates are changed', () => { + let trackingSpy; + + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z')); + findStartDatePicker().vm.$emit('close'); + }); + + it('mutation is called to update dates', () => { + expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + startAndDueDateWidget: { + dueDate: new Date('2022-12-31T00:00:00.000Z'), + startDate: new Date('2022-01-01T00:00:00.000Z'), + }, + }, + }); + }); + + it('start date input is disabled', () => { + expect(findStartDatePicker().props('disabled')).toBe(true); + }); + + it('due date input is disabled', () => { + expect(findDueDatePicker().props('disabled')).toBe(true); + }); + + it('tracks updating the dates', () => { + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_dates', { + category: TRACKING_CATEGORY_SHOW, + label: 'item_dates', + property: 'type_Task', + }); + }); + }); + + describe('when dates are unchanged', () => { + beforeEach(() => { + createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' }); + + findStartDatePicker().vm.$emit('input', new Date('2022-12-31T00:00:00.000Z')); + findStartDatePicker().vm.$emit('close'); + }); + + it('mutation is not called to update dates', () => { + expect(updateWorkItemMutationHandler).not.toHaveBeenCalled(); + }); + }); + + describe.each` + description | mutationHandler + ${'when there is a GraphQL error'} | ${jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse)} + ${'when there is a network error'} | ${jest.fn().mockRejectedValue(new Error())} + `('$description', ({ mutationHandler }) => { + beforeEach(() => { + createComponent({ + canUpdate: true, + dueDate: '2022-12-31', + startDate: '2022-12-31', + mutationHandler, + }); + + findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z')); + findStartDatePicker().vm.$emit('close'); + return waitForPromises(); + }); + + it('emits an error', () => { + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong while updating the task. Please try again.'], + ]); + }); + }); + }); + }); + + describe('when cannot update', () => { + it('start and due date inputs are disabled', async () => { + createComponent({ canUpdate: false, dueDate: '2022-01-01', startDate: '2022-01-01' }); + await nextTick(); + + expect(findStartDateInput().props('disabled')).toBe(true); + expect(findDueDateInput().props('disabled')).toBe(true); + }); + + describe('when there is no start and due date', () => { + it('shows None', () => { + createComponent({ canUpdate: false, dueDate: null, startDate: null }); + + expect(findGlFormGroup().text()).toContain(WorkItemDueDate.i18n.none); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js index d5f6921c2bc..887c5f615e9 100644 --- a/spec/frontend/work_items/components/work_item_information_spec.js +++ b/spec/frontend/work_items/components/work_item_information_spec.js @@ -8,7 +8,6 @@ const createComponent = () => mount(WorkItemInformation); describe('Work item information alert', () => { let wrapper; const tasksHelpPath = helpPagePath('user/tasks'); - const workItemsHelpPath = helpPagePath('development/work_items'); const findAlert = () => wrapper.findComponent(GlAlert); const findHelpLink = () => wrapper.findComponent(GlLink); @@ -33,16 +32,12 @@ describe('Work item information alert', () => { expect(findAlert().props('variant')).toBe('tip'); }); - it('should have the correct text for primary button and link', () => { + it('should have the correct text for title', () => { expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle); - expect(findAlert().props('primaryButtonText')).toBe( - WorkItemInformation.i18n.learnTasksButtonText, - ); - expect(findAlert().props('primaryButtonLink')).toBe(tasksHelpPath); }); it('should have the correct link to work item link', () => { expect(findHelpLink().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe(workItemsHelpPath); + 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 1734b901d1a..1d976897c15 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -9,7 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import { i18n } from '~/work_items/constants'; -import { temporaryConfig, resolvers } from '~/work_items/graphql/provider'; +import { temporaryConfig, resolvers } from '~/graphql_shared/issuable_client'; import { projectLabelsResponse, mockLabels, workItemQueryResponse } from '../mock_data'; Vue.use(VueApollo); @@ -45,13 +45,11 @@ describe('WorkItemLabels component', () => { }); wrapper = mountExtended(WorkItemLabels, { - provide: { - fullPath: 'test-project-path', - }, propsData: { labels, workItemId, canUpdate, + fullPath: 'test-project-path', }, attachTo: document.body, apolloProvider, 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 new file mode 100644 index 00000000000..1d5472a0473 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -0,0 +1,122 @@ +import { GlButton, GlIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.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 { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data'; + +describe('WorkItemLinkChild', () => { + const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; + let wrapper; + + const createComponent = ({ + projectPath = 'gitlab-org/gitlab-test', + canUpdate = true, + issuableGid = WORK_ITEM_ID, + childItem = workItemTask, + } = {}) => { + wrapper = shallowMountExtended(WorkItemLinkChild, { + propsData: { + projectPath, + canUpdate, + issuableGid, + childItem, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents + ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'} + ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'} + `( + 'renders item status icon and tooltip when item status is `$status`', + ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => { + createComponent({ childItem }); + + const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon); + const statusTooltip = wrapper.findComponent(RichTimestampTooltip); + + expect(statusIcon.props('name')).toBe(statusIconName); + expect(statusIcon.classes()).toContain(statusIconColorClass); + expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp); + expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents); + }, + ); + + it('renders confidential icon when item is confidential', () => { + createComponent({ childItem: confidentialWorkItemTask }); + + const confidentialIcon = wrapper.findByTestId('confidential-icon'); + + expect(confidentialIcon.props('name')).toBe('eye-slash'); + expect(confidentialIcon.attributes('title')).toBe('Confidential'); + }); + + describe('item title', () => { + let titleEl; + + beforeEach(() => { + createComponent(); + + titleEl = wrapper.findComponent(GlButton); + }); + + it('renders item title', () => { + expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4'); + expect(titleEl.text()).toBe(workItemTask.title); + }); + + 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 }) => { + const eventObj = { + preventDefault: jest.fn(), + }; + titleEl.vm.$emit(event, eventObj); + + expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]); + }); + }); + + describe('item menu', () => { + let itemMenuEl; + + beforeEach(() => { + createComponent(); + + itemMenuEl = wrapper.findComponent(WorkItemLinksMenu); + }); + + it('renders work-item-links-menu', () => { + expect(itemMenuEl.exists()).toBe(true); + + expect(itemMenuEl.attributes()).toMatchObject({ + 'work-item-id': workItemTask.id, + 'parent-work-item-id': WORK_ITEM_ID, + }); + }); + + it('does not render work-item-links-menu when canUpdate is false', () => { + createComponent({ canUpdate: false }); + + expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false); + }); + + it('removeChild event on menu triggers `click-remove-child` event', () => { + itemMenuEl.vm.$emit('removeChild'); + + expect(wrapper.emitted('remove')).toEqual([[workItemTask.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 00f508f1548..876aedff08b 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 @@ -1,12 +1,13 @@ import Vue, { nextTick } from 'vue'; -import { GlButton, GlIcon, GlAlert } from '@gitlab/ui'; +import { GlAlert } 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 SidebarEventHub from '~/sidebar/event_hub'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.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 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'; @@ -20,6 +21,20 @@ import { Vue.use(VueApollo); +const issueConfidentialityResponse = (confidential = false) => ({ + data: { + workspace: { + id: '1', + __typename: 'Project', + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/4', + confidential, + }, + }, + }, +}); + describe('WorkItemLinks', () => { let wrapper; let mockApollo; @@ -36,18 +51,18 @@ describe('WorkItemLinks', () => { const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); - const findChildren = () => wrapper.findAll('[data-testid="links-child"]'); - const createComponent = async ({ data = {}, fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse), mutationHandler = mutationChangeParentHandler, + confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()), } = {}) => { mockApollo = createMockApollo( [ [getWorkItemLinksQuery, fetchHandler], [changeWorkItemParentMutation, mutationHandler], [workItemQuery, childWorkItemQueryHandler], + [issueConfidentialQuery, confidentialQueryHandler], ], {}, { addTypename: true }, @@ -61,6 +76,7 @@ describe('WorkItemLinks', () => { }, provide: { projectPath: 'project/path', + iid: '1', }, propsData: { issuableId: 1 }, apolloProvider: mockApollo, @@ -77,8 +93,9 @@ describe('WorkItemLinks', () => { const findLinksBody = () => wrapper.findByTestId('links-body'); const findEmptyState = () => wrapper.findByTestId('links-empty'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); + const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); + const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0); const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); - const findFirstLinksMenu = () => wrapper.findByTestId('links-menu'); const findChildrenCount = () => wrapper.findByTestId('children-count'); beforeEach(async () => { @@ -132,8 +149,7 @@ describe('WorkItemLinks', () => { it('renders all hierarchy widget children', () => { expect(findLinksBody().exists()).toBe(true); - expect(findChildren()).toHaveLength(4); - expect(findFirstLinksMenu().exists()).toBe(true); + expect(findWorkItemLinkChildItems()).toHaveLength(4); }); it('shows alert when list loading fails', async () => { @@ -148,40 +164,12 @@ describe('WorkItemLinks', () => { expect(findAlert().text()).toBe(errorMessage); }); - it('renders widget child icon and tooltip', () => { - expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m'); - expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close'); - }); - - it('renders confidentiality icon when child item is confidential', () => { - const children = wrapper.findAll('[data-testid="links-child"]'); - const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]'); - - expect(confidentialIcon.exists()).toBe(true); - expect(confidentialIcon.props('name')).toBe('eye-slash'); - }); - it('displays number if children', () => { expect(findChildrenCount().exists()).toBe(true); expect(findChildrenCount().text()).toContain('4'); }); - it('refetches child items when `confidentialityUpdated` event is emitted on SidebarEventhub', async () => { - const fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse); - await createComponent({ - fetchHandler, - }); - await waitForPromises(); - - SidebarEventHub.$emit('confidentialityUpdated'); - await nextTick(); - - // First call is done on component mount. - // Second call is done on confidentialityUpdated event. - expect(fetchHandler).toHaveBeenCalledTimes(2); - }); - describe('when no permission to update', () => { beforeEach(async () => { await createComponent({ @@ -194,17 +182,21 @@ describe('WorkItemLinks', () => { }); it('does not display link menu on children', () => { - expect(findFirstLinksMenu().exists()).toBe(false); + expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false); }); }); describe('remove child', () => { + let firstChild; + beforeEach(async () => { await createComponent({ mutationHandler: mutationChangeParentHandler }); + + firstChild = findFirstWorkItemLinkChild(); }); it('calls correct mutation with correct variables', async () => { - findFirstLinksMenu().vm.$emit('removeChild'); + firstChild.vm.$emit('remove', firstChild.vm.childItem.id); await waitForPromises(); @@ -219,7 +211,7 @@ describe('WorkItemLinks', () => { }); it('shows toast when mutation succeeds', async () => { - findFirstLinksMenu().vm.$emit('removeChild'); + firstChild.vm.$emit('remove', firstChild.vm.childItem.id); await waitForPromises(); @@ -229,28 +221,30 @@ describe('WorkItemLinks', () => { }); it('renders correct number of children after removal', async () => { - expect(findChildren()).toHaveLength(4); + expect(findWorkItemLinkChildItems()).toHaveLength(4); - findFirstLinksMenu().vm.$emit('removeChild'); + firstChild.vm.$emit('remove', firstChild.vm.childItem.id); await waitForPromises(); - expect(findChildren()).toHaveLength(3); + expect(findWorkItemLinkChildItems()).toHaveLength(3); }); }); describe('prefetching child items', () => { + let firstChild; + beforeEach(async () => { await createComponent(); - }); - const findChildLink = () => findChildren().at(0).findComponent(GlButton); + firstChild = findFirstWorkItemLinkChild(); + }); it('does not fetch the child work item before hovering work item links', () => { expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); }); it('fetches the child work item if link is hovered for 250+ ms', async () => { - findChildLink().vm.$emit('mouseover'); + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); await waitForPromises(); @@ -260,12 +254,24 @@ describe('WorkItemLinks', () => { }); it('does not fetch the child work item if link is hovered for less than 250 ms', async () => { - findChildLink().vm.$emit('mouseover'); + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); jest.advanceTimersByTime(200); - findChildLink().vm.$emit('mouseout'); + firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); await waitForPromises(); expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); }); }); + + describe('when parent item is confidential', () => { + it('passes correct confidentiality status to form', async () => { + await createComponent({ + confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)), + }); + findToggleAddFormButton().vm.$emit('click'); + await nextTick(); + + expect(findAddLinksForm().props('parentConfidential')).toBe(true); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js index 6b23a6e4795..b24d940d56a 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises'; import ItemState from '~/work_items/components/item_state.vue'; import WorkItemState from '~/work_items/components/work_item_state.vue'; import { - i18n, STATE_OPEN, STATE_CLOSED, STATE_EVENT_CLOSE, @@ -104,7 +103,9 @@ describe('WorkItemState component', () => { findItemState().vm.$emit('changed', STATE_CLOSED); await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong while updating the task. Please try again.'], + ]); }); it('tracks editing the state', async () => { diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index c0d966abab8..a549aad5cd8 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -6,7 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper'; import waitForPromises from 'helpers/wait_for_promises'; import ItemTitle from '~/work_items/components/item_title.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; -import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data'; @@ -116,7 +116,9 @@ describe('WorkItemTitle component', () => { findItemTitle().vm.$emit('title-changed', 'new title'); await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong while updating the task. Please try again.'], + ]); }); it('tracks editing the title', async () => { diff --git a/spec/frontend/work_items/components/work_item_type_icon_spec.js b/spec/frontend/work_items/components/work_item_type_icon_spec.js index 85466578e18..95ddfc3980e 100644 --- a/spec/frontend/work_items/components/work_item_type_icon_spec.js +++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js @@ -1,11 +1,17 @@ import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; let wrapper; function createComponent(propsData) { - wrapper = shallowMount(WorkItemTypeIcon, { propsData }); + wrapper = shallowMount(WorkItemTypeIcon, { + propsData, + directives: { + GlTooltip: createMockDirective(), + }, + }); } describe('Work Item type component', () => { @@ -16,22 +22,23 @@ describe('Work Item type component', () => { }); describe.each` - workItemType | workItemIconName | iconName | text - ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} - ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} - ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} - ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} - ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} - ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} - ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} - ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} + workItemType | workItemIconName | iconName | text | showTooltipOnHover + ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false} + ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} | ${true} + ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true} + ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} | ${true} + ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true} + ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false} + ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true} + ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} | ${true} `( 'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"', - ({ workItemType, workItemIconName, iconName, text }) => { + ({ workItemType, workItemIconName, iconName, text, showTooltipOnHover }) => { beforeEach(() => { createComponent({ workItemType, workItemIconName, + showTooltipOnHover, }); }); @@ -42,6 +49,16 @@ describe('Work Item type component', () => { it(`renders correct text`, () => { expect(wrapper.text()).toBe(text); }); + + it('renders the icon in gray color', () => { + expect(findIcon().classes()).toContain('gl-text-gray-500'); + }); + + it('shows tooltip on hover when props passed', () => { + const tooltip = getBinding(findIcon().element, 'gl-tooltip'); + + expect(tooltip.value).toBe(showTooltipOnHover); + }); }, ); }); diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js deleted file mode 100644 index 94bdb336deb..00000000000 --- a/spec/frontend/work_items/components/work_item_weight_spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import { GlForm, GlFormInput } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockTracking } from 'helpers/tracking_helper'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { __ } from '~/locale'; -import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; -import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; -import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data'; - -describe('WorkItemWeight component', () => { - Vue.use(VueApollo); - - let wrapper; - - const workItemId = 'gid://gitlab/WorkItem/1'; - const workItemType = 'Task'; - - const findForm = () => wrapper.findComponent(GlForm); - const findInput = () => wrapper.findComponent(GlFormInput); - - const createComponent = ({ - canUpdate = false, - hasIssueWeightsFeature = true, - isEditing = false, - weight, - mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse), - } = {}) => { - wrapper = mountExtended(WorkItemWeight, { - apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), - propsData: { - canUpdate, - weight, - workItemId, - workItemType, - }, - provide: { - hasIssueWeightsFeature, - }, - }); - - if (isEditing) { - findInput().vm.$emit('focus'); - } - }; - - describe('`issue_weights` licensed feature', () => { - describe.each` - description | hasIssueWeightsFeature | exists - ${'when available'} | ${true} | ${true} - ${'when not available'} | ${false} | ${false} - `('$description', ({ hasIssueWeightsFeature, exists }) => { - it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => { - createComponent({ hasIssueWeightsFeature }); - - expect(findForm().exists()).toBe(exists); - }); - }); - }); - - describe('weight input', () => { - it('has "Weight" label', () => { - createComponent(); - - expect(wrapper.findByLabelText(__('Weight')).exists()).toBe(true); - }); - - describe('placeholder attribute', () => { - describe.each` - description | isEditing | canUpdate | value - ${'when not editing and cannot update'} | ${false} | ${false} | ${__('None')} - ${'when editing and cannot update'} | ${true} | ${false} | ${__('None')} - ${'when not editing and can update'} | ${false} | ${true} | ${__('None')} - ${'when editing and can update'} | ${true} | ${true} | ${__('Enter a number')} - `('$description', ({ isEditing, canUpdate, value }) => { - it(`has a value of "${value}"`, async () => { - createComponent({ canUpdate, isEditing }); - await nextTick(); - - expect(findInput().attributes('placeholder')).toBe(value); - }); - }); - }); - - describe('readonly attribute', () => { - describe.each` - description | canUpdate | value - ${'when cannot update'} | ${false} | ${'readonly'} - ${'when can update'} | ${true} | ${undefined} - `('$description', ({ canUpdate, value }) => { - it(`renders readonly=${value}`, () => { - createComponent({ canUpdate }); - - expect(findInput().attributes('readonly')).toBe(value); - }); - }); - }); - - describe('type attribute', () => { - describe.each` - description | isEditing | canUpdate | type - ${'when not editing and cannot update'} | ${false} | ${false} | ${'text'} - ${'when editing and cannot update'} | ${true} | ${false} | ${'text'} - ${'when not editing and can update'} | ${false} | ${true} | ${'text'} - ${'when editing and can update'} | ${true} | ${true} | ${'number'} - `('$description', ({ isEditing, canUpdate, type }) => { - it(`has a value of "${type}"`, async () => { - createComponent({ canUpdate, isEditing }); - await nextTick(); - - expect(findInput().attributes('type')).toBe(type); - }); - }); - }); - - describe('value attribute', () => { - describe.each` - weight | value - ${1} | ${'1'} - ${0} | ${'0'} - ${null} | ${''} - ${undefined} | ${''} - `('when `weight` prop is "$weight"', ({ weight, value }) => { - it(`value is "${value}"`, () => { - createComponent({ weight }); - - expect(findInput().element.value).toBe(value); - }); - }); - }); - - describe('when blurred', () => { - it('calls a mutation to update the weight when the input value is different', () => { - const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ - isEditing: true, - weight: 0, - mutationHandler: mutationSpy, - canUpdate: true, - }); - - findInput().vm.$emit('blur', { target: { value: 1 } }); - - expect(mutationSpy).toHaveBeenCalledWith({ - input: { - id: workItemId, - weightWidget: { - weight: 1, - }, - }, - }); - }); - - it('does not call a mutation to update the weight when the input value is the same', () => { - const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true }); - - findInput().trigger('blur'); - - expect(mutationSpy).not.toHaveBeenCalledWith(); - }); - - it('emits an error when there is a GraphQL error', async () => { - const response = { - data: { - workItemUpdate: { - errors: ['Error!'], - workItem: {}, - }, - }, - }; - createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockResolvedValue(response), - canUpdate: true, - }); - - findInput().trigger('blur'); - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); - }); - - it('emits an error when there is a network error', async () => { - createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockRejectedValue(new Error()), - canUpdate: true, - }); - - findInput().trigger('blur'); - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); - }); - - it('tracks updating the weight', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - createComponent({ canUpdate: true }); - - findInput().trigger('blur'); - - expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', { - category: TRACKING_CATEGORY_SHOW, - label: 'item_weight', - property: 'type_Task', - }); - }); - }); - }); -}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index d24ac2a9f93..e1bc8d2f6b7 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -28,6 +28,11 @@ export const workItemQueryResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -93,6 +98,11 @@ export const updateWorkItemMutationResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -128,6 +138,16 @@ export const updateWorkItemMutationResponse = { }, }; +export const updateWorkItemMutationErrorResponse = { + data: { + workItemUpdate: { + __typename: 'WorkItemUpdatePayload', + errors: ['Error!'], + workItem: {}, + }, + }, +}; + export const mockParent = { parent: { id: 'gid://gitlab/Issue/1', @@ -142,6 +162,7 @@ export const workItemResponseFactory = ({ canDelete = false, allowsMultipleAssignees = true, assigneesWidgetPresent = true, + datesWidgetPresent = true, weightWidgetPresent = true, confidential = false, canInviteMembers = false, @@ -157,6 +178,11 @@ export const workItemResponseFactory = ({ confidential, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -186,6 +212,14 @@ export const workItemResponseFactory = ({ }, } : { type: 'MOCK TYPE' }, + datesWidgetPresent + ? { + __typename: 'WorkItemWidgetStartAndDueDate', + type: 'START_AND_DUE_DATE', + dueDate: '2022-12-31', + startDate: '2022-01-01', + } + : { type: 'MOCK TYPE' }, weightWidgetPresent ? { __typename: 'WorkItemWidgetWeight', @@ -212,17 +246,6 @@ export const workItemResponseFactory = ({ }, }); -export const updateWorkItemWidgetsResponse = { - data: { - workItemUpdateWidgets: { - workItem: { - id: 1234, - }, - errors: [], - }, - }, -}; - export const projectWorkItemTypesQueryResponse = { data: { workspace: { @@ -251,6 +274,11 @@ export const createWorkItemMutationResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -282,6 +310,11 @@ export const createWorkItemFromTaskMutationResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -310,6 +343,11 @@ export const createWorkItemFromTaskMutationResponse = { closedAt: null, description: '', confidential: false, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -368,6 +406,21 @@ export const deleteWorkItemFromTaskMutationErrorResponse = { }, }; +export const workItemDatesSubscriptionResponse = { + data: { + issuableDatesUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetStartAndDueDate', + dueDate: '2022-12-31', + startDate: '2022-01-01', + }, + ], + }, + }, +}; + export const workItemTitleSubscriptionResponse = { data: { issuableTitleUpdated: { @@ -377,6 +430,20 @@ export const workItemTitleSubscriptionResponse = { }, }; +export const workItemWeightSubscriptionResponse = { + data: { + issuableWeightUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetWeight', + weight: 1, + }, + ], + }, + }, +}; + export const workItemHierarchyEmptyResponse = { data: { workItem: { @@ -388,6 +455,11 @@ export const workItemHierarchyEmptyResponse = { title: 'New title', createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, userPermissions: { deleteWorkItem: false, updateWorkItem: false, @@ -426,6 +498,11 @@ export const workItemHierarchyNoUpdatePermissionResponse = { deleteWorkItem: false, updateWorkItem: false, }, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, confidential: false, widgets: [ { @@ -461,6 +538,48 @@ export const workItemHierarchyNoUpdatePermissionResponse = { }, }; +export const workItemTask = { + id: 'gid://gitlab/WorkItem/4', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'bar', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', +}; + +export const confidentialWorkItemTask = { + id: 'gid://gitlab/WorkItem/2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'xyz', + state: 'OPEN', + confidential: true, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', +}; + +export const closedWorkItemTask = { + id: 'gid://gitlab/WorkItem/3', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'abc', + state: 'CLOSED', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: '2022-08-12T13:07:52Z', + __typename: 'WorkItem', +}; + export const workItemHierarchyResponse = { data: { workItem: { @@ -475,6 +594,11 @@ export const workItemHierarchyResponse = { updateWorkItem: true, }, confidential: false, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, widgets: [ { type: 'DESCRIPTION', @@ -485,45 +609,9 @@ export const workItemHierarchyResponse = { parent: null, children: { nodes: [ - { - id: 'gid://gitlab/WorkItem/2', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'xyz', - state: 'OPEN', - confidential: true, - createdAt: '2022-08-03T12:41:54Z', - closedAt: null, - __typename: 'WorkItem', - }, - { - id: 'gid://gitlab/WorkItem/3', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'abc', - state: 'CLOSED', - confidential: false, - createdAt: '2022-08-03T12:41:54Z', - closedAt: '2022-08-12T13:07:52Z', - __typename: 'WorkItem', - }, - { - id: 'gid://gitlab/WorkItem/4', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'bar', - state: 'OPEN', - confidential: false, - createdAt: '2022-08-03T12:41:54Z', - closedAt: null, - __typename: 'WorkItem', - }, + confidentialWorkItemTask, + closedWorkItemTask, + workItemTask, { id: 'gid://gitlab/WorkItem/5', workItemType: { @@ -570,6 +658,11 @@ export const changeWorkItemParentMutationResponse = { confidential: false, createdAt: '2022-08-03T12:41:54Z', closedAt: null, + project: { + __typename: 'Project', + id: '1', + fullPath: 'test-project-path', + }, widgets: [ { __typename: 'WorkItemWidgetHierarchy', @@ -649,6 +742,71 @@ export const projectMembersResponseWithCurrentUser = { }, }, ], + pageInfo: { + hasNextPage: false, + endCursor: null, + startCursor: null, + }, + }, + }, + }, +}; + +export const projectMembersResponseWithCurrentUserWithNextPage = { + data: { + workspace: { + id: '1', + __typename: 'Project', + users: { + nodes: [ + { + id: 'user-2', + user: { + __typename: 'UserCore', + id: 'gid://gitlab/User/5', + avatarUrl: '/avatar2', + name: 'rookie', + username: 'rookie', + webUrl: 'rookie', + status: null, + }, + }, + { + id: 'user-1', + user: { + __typename: 'UserCore', + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: '/root', + status: null, + }, + }, + ], + pageInfo: { + hasNextPage: true, + endCursor: 'endCursor', + startCursor: 'startCursor', + }, + }, + }, + }, +}; + +export const projectMembersResponseWithNoMatchingUsers = { + data: { + workspace: { + id: '1', + __typename: 'Project', + users: { + nodes: [], + pageInfo: { + endCursor: null, + hasNextPage: false, + startCursor: null, + }, }, }, }, 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 fed8be3783a..15dac25b7d9 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -193,6 +193,8 @@ describe('Create work item component', () => { wrapper.find('form').trigger('submit'); await waitForPromises(); - expect(findAlert().text()).toBe(CreateWorkItem.createErrorText); + expect(findAlert().text()).toBe( + 'Something went wrong when creating work item. Please try again.', + ); }); }); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 99dcd886f7b..ab370e2ca8b 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -1,5 +1,18 @@ 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 { + workItemDatesSubscriptionResponse, + workItemResponseFactory, + workItemTitleSubscriptionResponse, + workItemWeightSubscriptionResponse, +} from 'jest/work_items/mock_data'; import App from '~/work_items/components/app.vue'; +import workItemQuery from '~/work_items/graphql/work_item.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 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'; @@ -7,26 +20,36 @@ import { createRouter } from '~/work_items/router'; describe('Work items router', () => { let wrapper; + Vue.use(VueApollo); + + 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 createComponent = async (routeArg) => { const router = createRouter('/work_item'); if (routeArg !== undefined) { await router.push(routeArg); } + const handlers = [ + [workItemQuery, workItemQueryHandler], + [workItemDatesSubscription, datesSubscriptionHandler], + [workItemTitleSubscription, titleSubscriptionHandler], + ]; + + if (IS_EE) { + handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); + } + wrapper = mount(App, { + apolloProvider: createMockApollo(handlers), router, provide: { fullPath: 'full-path', issuesListPath: 'full-path/-/issues', }, - mocks: { - $apollo: { - queries: { - workItem: {}, - workItemTypes: {}, - }, - }, - }, }); }; |