diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 6d310ed973cfd073ceba5afd6877892ed1b60918..831cef6683615b0753969632fc82b363b8aba594 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -8,7 +8,7 @@ import { } from '@gitlab/ui'; import $ from 'jquery'; import Vue from 'vue'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import createFlash from '~/flash'; import { isPositiveInteger } from '~/lib/utils/number_utils'; @@ -140,7 +140,10 @@ export default { } if (this.workItemId) { - this.$refs.detailsModal.show(); + const taskLink = this.$el.querySelector( + `.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`, + ); + this.openWorkItemDetailModal(taskLink); } }, methods: { @@ -216,7 +219,7 @@ export default { this.addHoverListeners(taskLink, workItemId); taskLink.addEventListener('click', (e) => { e.preventDefault(); - this.$refs.detailsModal.show(); + this.openWorkItemDetailModal(taskLink); this.workItemId = workItemId; this.updateWorkItemIdUrlQuery(issue); this.track('viewed_work_item_from_modal', { @@ -248,7 +251,7 @@ export default { </svg> `; button.setAttribute('aria-label', s__('WorkItem|Convert to work item')); - button.addEventListener('click', () => this.openCreateTaskModal(button.id)); + button.addEventListener('click', () => this.openCreateTaskModal(button)); item.prepend(button); }); }, @@ -265,20 +268,29 @@ export default { } }); }, - openCreateTaskModal(id) { - const { parentElement } = this.$el.querySelector(`#${id}`); + setActiveTask(el) { + const { parentElement } = el; const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g); this.activeTask = { - id, title: parentElement.innerText, lineNumberStart: lineNumbers[0], lineNumberEnd: lineNumbers[1], }; + }, + openCreateTaskModal(el) { + this.setActiveTask(el); this.$refs.modal.show(); }, closeCreateTaskModal() { this.$refs.modal.hide(); }, + openWorkItemDetailModal(el) { + if (!el) { + return; + } + this.setActiveTask(el); + this.$refs.detailsModal.show(); + }, closeWorkItemDetailModal() { this.workItemId = undefined; this.updateWorkItemIdUrlQuery(undefined); @@ -287,7 +299,8 @@ export default { this.$emit('updateDescription', description); this.closeCreateTaskModal(); }, - handleDeleteTask() { + handleDeleteTask(description) { + this.$emit('updateDescription', description); this.$toast.show(s__('WorkItem|Work item deleted')); }, updateWorkItemIdUrlQuery(workItemId) { @@ -353,6 +366,10 @@ export default { ref="detailsModal" :can-update="canUpdate" :work-item-id="workItemId" + :issue-gid="issueGid" + :lock-version="lockVersion" + :line-number-start="activeTask.lineNumberStart" + :line-number-end="activeTask.lineNumberEnd" @workItemDeleted="handleDeleteTask" @close="closeWorkItemDetailModal" /> diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index 701cb84df59e3ccdb27c820a2bb2cc6e5ddb546c..31e4a932c5af684f248e2a11c91346435a3ae6c3 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -1,7 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; -import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql'; +import Tracking from '~/tracking'; export default { i18n: { @@ -15,6 +15,7 @@ export default { directives: { GlModal: GlModalDirective, }, + mixins: [Tracking.mixin({ label: 'actions_menu' })], props: { workItemId: { type: String, @@ -27,36 +28,16 @@ export default { default: false, }, }, - emits: ['workItemDeleted', 'error'], + emits: ['deleteWorkItem'], methods: { - deleteWorkItem() { - this.$apollo - .mutate({ - mutation: deleteWorkItemMutation, - variables: { - input: { - id: this.workItemId, - }, - }, - }) - .then(({ data: { workItemDelete, errors } }) => { - if (errors?.length) { - throw new Error(errors[0].message); - } - - if (workItemDelete?.errors.length) { - throw new Error(workItemDelete.errors[0]); - } - - this.$emit('workItemDeleted'); - }) - .catch((e) => { - this.$emit( - 'error', - e.message || - s__('WorkItem|Something went wrong when deleting the work item. Please try again.'), - ); - }); + handleDeleteWorkItem() { + this.track('click_delete_work_item'); + this.$emit('deleteWorkItem'); + }, + handleCancelDeleteWorkItem({ trigger }) { + if (trigger !== 'ok') { + this.track('cancel_delete_work_item'); + } }, }, }; @@ -81,7 +62,8 @@ export default { :title="$options.i18n.deleteWorkItem" :ok-title="$options.i18n.deleteWorkItem" ok-variant="danger" - @ok="deleteWorkItem" + @ok="handleDeleteWorkItem" + @hide="handleCancelDeleteWorkItem" > {{ s__( diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 33d49583b04aafccbea0727bfbdefd493b98a25f..4222ffe42fe739fd3b7b5d637e13512b11fe0a6d 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -67,11 +67,6 @@ export default { return this.workItem?.userPermissions?.deleteWorkItem; }, }, - methods: { - handleWorkItemDeleted() { - this.$emit('workItemDeleted'); - }, - }, }; </script> @@ -101,7 +96,7 @@ export default { :work-item-id="workItem.id" :can-delete="canDelete" class="gl-ml-auto gl-mt-5" - @workItemDeleted="handleWorkItemDeleted" + @deleteWorkItem="$emit('deleteWorkItem')" @error="error = $event" /> </div> diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue index 693a7649508ed49bb620496fa9efc70c44c78789..172a40a6e56fd5275703bb05aa52efd26ca786e4 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue @@ -1,5 +1,7 @@ <script> import { GlAlert, GlModal } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import deleteWorkItemFromTaskMutation from '../graphql/delete_task_from_work_item.mutation.graphql'; import WorkItemDetail from './work_item_detail.vue'; export default { @@ -14,17 +16,72 @@ export default { required: false, default: null, }, + issueGid: { + type: String, + required: false, + default: '', + }, + lockVersion: { + type: Number, + required: false, + default: null, + }, + lineNumberStart: { + type: String, + required: false, + default: null, + }, + lineNumberEnd: { + type: String, + required: false, + default: null, + }, }, - emits: ['workItemDeleted', 'close'], + emits: ['workItemDeleted', 'workItemUpdated', 'close'], data() { return { error: undefined, }; }, methods: { - handleWorkItemDeleted() { - this.$emit('workItemDeleted'); - this.closeModal(); + deleteWorkItem() { + this.$apollo + .mutate({ + mutation: deleteWorkItemFromTaskMutation, + variables: { + input: { + id: this.issueGid, + lockVersion: this.lockVersion, + taskData: { + id: this.workItemId, + lineNumberStart: Number(this.lineNumberStart), + lineNumberEnd: Number(this.lineNumberEnd), + }, + }, + }, + }) + .then( + ({ + data: { + workItemDeleteTask: { + workItem: { descriptionHtml }, + errors, + }, + }, + }) => { + if (errors?.length) { + throw new Error(errors[0].message); + } + + this.$emit('workItemDeleted', descriptionHtml); + this.$refs.modal.hide(); + }, + ) + .catch((e) => { + this.error = + e.message || + s__('WorkItem|Something went wrong when deleting the work item. Please try again.'); + }); }, closeModal() { this.error = ''; @@ -46,7 +103,11 @@ export default { {{ error }} </gl-alert> - <work-item-detail :work-item-id="workItemId" @workItemDeleted="handleWorkItemDeleted" /> + <work-item-detail + :work-item-id="workItemId" + @deleteWorkItem="deleteWorkItem" + @workItemUpdated="$emit('workItemUpdated')" + /> </gl-modal> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state.vue index 7d4b48f847f958cd29a888673fdb9aab5d0ad5a6..51db4c804eb3e1d3f465139a78be9433962cf793 100644 --- a/app/assets/javascripts/work_items/components/work_item_state.vue +++ b/app/assets/javascripts/work_items/components/work_item_state.vue @@ -75,6 +75,8 @@ export default { if (workItemUpdate?.errors?.length) { throw new Error(workItemUpdate.errors[0]); } + + this.$emit('updated'); } catch (error) { this.$emit('error', i18n.updateError); Sentry.captureException(error); diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue index 73b46bb06d227e37203f77afcb121b23b38cd697..d2e6d3c0bbf2d17e0fa8c225575ee2bf1d753582 100644 --- a/app/assets/javascripts/work_items/components/work_item_title.vue +++ b/app/assets/javascripts/work_items/components/work_item_title.vue @@ -52,6 +52,7 @@ export default { }, }); this.track('updated_title'); + this.$emit('updated'); } catch { this.$emit('error', i18n.updateError); } diff --git a/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql new file mode 100644 index 0000000000000000000000000000000000000000..32c07ed48c72d8c7728c1efd5ccebf32c1a952c8 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql @@ -0,0 +1,9 @@ +mutation workItemDeleteTask($input: WorkItemDeleteTaskInput!) { + workItemDeleteTask(input: $input) { + workItem { + id + descriptionHtml + } + errors + } +} diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql index ca5ba7a7d8eec2ee904d6be2e66c6caf2070a0fe..e25fd102699b7632a96adbd01ec726bf144ac141 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql @@ -2,6 +2,7 @@ fragment WorkItem on WorkItem { id title state + description workItemType { id name diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index 10fae9b9cc002be678f5a2987a79ed6e3dbddb1b..e39b0d6a3539cd7f7d98f9722282ad4a09e8ba86 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -5,7 +5,7 @@ import { createApolloProvider } from './graphql/provider'; export const initWorkItemsRoot = () => { const el = document.querySelector('#js-work-items'); - const { fullPath } = el.dataset; + const { fullPath, issuesListPath } = el.dataset; return new Vue({ el, @@ -13,6 +13,7 @@ export const initWorkItemsRoot = () => { apolloProvider: createApolloProvider(), provide: { fullPath, + issuesListPath, }, render(createElement) { return createElement(App); diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue index b8ce6d641a9eac68ef09a7c49a9ba0a13dc33a1b..6dc3dc3b3c9c7c3e85d033844592c05c2eed9f16 100644 --- a/app/assets/javascripts/work_items/pages/work_item_root.vue +++ b/app/assets/javascripts/work_items/pages/work_item_root.vue @@ -1,33 +1,70 @@ <script> +import { GlAlert } from '@gitlab/ui'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { visitUrl } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import WorkItemDetail from '../components/work_item_detail.vue'; +import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql'; export default { components: { + GlAlert, WorkItemDetail, }, + inject: ['issuesListPath'], props: { id: { type: String, required: true, }, }, + data() { + return { + error: '', + }; + }, computed: { gid() { return convertToGraphQLId(TYPE_WORK_ITEM, this.id); }, }, methods: { - handleWorkItemDeleted() { - this.$root.$toast.show(s__('WorkItem|Work item deleted')); - this.$router.push('/'); + deleteWorkItem() { + this.$apollo + .mutate({ + mutation: deleteWorkItemMutation, + variables: { + input: { + id: this.gid, + }, + }, + }) + .then(({ data: { workItemDelete, errors } }) => { + if (errors?.length) { + throw new Error(errors[0].message); + } + + if (workItemDelete?.errors.length) { + throw new Error(workItemDelete.errors[0]); + } + + this.$toast.show(s__('WorkItem|Work item deleted')); + visitUrl(this.issuesListPath); + }) + .catch((e) => { + this.error = + e.message || + s__('WorkItem|Something went wrong when deleting the work item. Please try again.'); + }); }, }, }; </script> <template> - <work-item-detail :work-item-id="gid" @workItemDeleted="handleWorkItemDeleted" /> + <div> + <gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert> + <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" /> + </div> </template> diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml index 0efd7a740d39fbca075b4964370f720b612becb1..356f93c6ed50a26f3cc40d00f88887b12696e2e6 100644 --- a/app/views/projects/work_items/index.html.haml +++ b/app/views/projects/work_items/index.html.haml @@ -1,3 +1,3 @@ - page_title s_('WorkItem|Work Items') -#js-work-items{ data: { full_path: @project.full_path } } +#js-work-items{ data: { full_path: @project.full_path, issues_list_path: project_issues_path(@project) } } diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index c08453530e5ef0abe546c66d4a1bd5ecb35b0e42..1ae04531a6b82afaec7403322784d1c933a8b820 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -37,6 +37,7 @@ const showDetailsModal = jest.fn(); const $toast = { show: jest.fn(), }; + const workItemQueryResponse = { data: { workItem: null, @@ -319,8 +320,10 @@ describe('Description component', () => { }); it('shows toast after delete success', async () => { - findWorkItemDetailModal().vm.$emit('workItemDeleted'); + const newDesc = 'description'; + findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); + expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); expect($toast.show).toHaveBeenCalledWith('Work item deleted'); }); }); @@ -381,7 +384,8 @@ describe('Description component', () => { describe('when url query `work_item_id` exists', () => { it.each` behavior | workItemId | modalOpened - ${'opens'} | ${'123'} | ${1} + ${'opens'} | ${'2'} | ${1} + ${'does not open'} | ${'123'} | ${0} ${'does not open'} | ${'123e'} | ${0} ${'does not open'} | ${'12e3'} | ${0} ${'does not open'} | ${'1e23'} | ${0} 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 286c8180e160b512495a16da64824cba11d827e3..137a0a7326d445cbcf42d00dfa17c9d4338748b6 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -1,29 +1,17 @@ import { GlDropdownItem, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; -import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql'; -import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data'; describe('WorkItemActions component', () => { let wrapper; let glModalDirective; - Vue.use(VueApollo); - const findModal = () => wrapper.findComponent(GlModal); const findDeleteButton = () => wrapper.findComponent(GlDropdownItem); - const createComponent = ({ - canDelete = true, - deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse), - } = {}) => { + const createComponent = ({ canDelete = true } = {}) => { glModalDirective = jest.fn(); wrapper = shallowMount(WorkItemActions, { - apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]), propsData: { workItemId: '123', canDelete }, directives: { glModal: { @@ -54,43 +42,12 @@ describe('WorkItemActions component', () => { expect(glModalDirective).toHaveBeenCalled(); }); - it('calls delete mutation when clicking OK button', () => { - const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse); - - createComponent({ - deleteWorkItemHandler, - }); - - findModal().vm.$emit('ok'); - - expect(deleteWorkItemHandler).toHaveBeenCalled(); - expect(wrapper.emitted('error')).toBeUndefined(); - }); - - it('emits event after delete success', async () => { + it('emits event when clicking OK button', () => { createComponent(); findModal().vm.$emit('ok'); - await waitForPromises(); - - expect(wrapper.emitted('workItemDeleted')).not.toBeUndefined(); - expect(wrapper.emitted('error')).toBeUndefined(); - }); - - it('emits error event after delete failure', async () => { - createComponent({ - deleteWorkItemHandler: jest.fn().mockResolvedValue(deleteWorkItemFailureResponse), - }); - - findModal().vm.$emit('ok'); - - await waitForPromises(); - - expect(wrapper.emitted('error')[0]).toEqual([ - "The resource that you are attempting to access does not exist or you don't have permission to perform this action", - ]); - expect(wrapper.emitted('workItemDeleted')).toBeUndefined(); + expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]); }); it('does not render when canDelete is false', () => { 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 67d794519b61fb6e1915d30d1092db45883992b3..aaabdbc82d978fc97621cab5327cd1dee8ff2ffc 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 @@ -1,21 +1,51 @@ -import { GlModal, GlAlert } from '@gitlab/ui'; +import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; +import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql'; describe('WorkItemDetailModal component', () => { let wrapper; Vue.use(VueApollo); + const hideModal = jest.fn(); + const GlModal = { + template: ` + <div> + <slot></slot> + </div> + `, + methods: { + hide: hideModal, + }, + }; + const findModal = () => wrapper.findComponent(GlModal); const findAlert = () => wrapper.findComponent(GlAlert); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); const createComponent = ({ workItemId = '1', error = false } = {}) => { + const apolloProvider = createMockApollo([ + [ + deleteWorkItemFromTaskMutation, + jest.fn().mockResolvedValue({ + data: { + workItemDeleteTask: { + workItem: { id: 123, descriptionHtml: 'updated work item desc' }, + errors: [], + }, + }, + }), + ], + ]); + wrapper = shallowMount(WorkItemDetailModal, { + apolloProvider, propsData: { workItemId }, data() { return { @@ -35,7 +65,9 @@ describe('WorkItemDetailModal component', () => { it('renders WorkItemDetail', () => { createComponent(); - expect(findWorkItemDetail().props()).toEqual({ workItemId: '1' }); + expect(findWorkItemDetail().props()).toEqual({ + workItemId: '1', + }); }); it('renders alert if there is an error', () => { @@ -65,10 +97,24 @@ describe('WorkItemDetailModal component', () => { expect(wrapper.emitted('close')).toBeTruthy(); }); - it('emits `workItemDeleted` event on deleting work item', () => { + it('emits `workItemUpdated` event on updating work item', () => { createComponent(); - findWorkItemDetail().vm.$emit('workItemDeleted'); + findWorkItemDetail().vm.$emit('workItemUpdated'); + + expect(wrapper.emitted('workItemUpdated')).toBeTruthy(); + }); + + describe('delete work item', () => { + it('emits workItemDeleted and closes modal', async () => { + createComponent(); + const newDesc = 'updated work item desc'; + + findWorkItemDetail().vm.$emit('deleteWorkItem'); - expect(wrapper.emitted('workItemDeleted')).toBeTruthy(); + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]); + expect(hideModal).toHaveBeenCalled(); + }); }); }); 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 6584d197206da9c4fa4d877f75bc580cd8f53eb0..9e48f56d9e930b9c79b7ecd6eccc68e113ebf811 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -81,6 +81,15 @@ describe('WorkItemState component', () => { }); }); + it('emits updated event', async () => { + createComponent(); + + findItemState().vm.$emit('changed', STATE_CLOSED); + await waitForPromises(); + + expect(wrapper.emitted('updated')).toEqual([[]]); + }); + it('emits an error message when the mutation was unsuccessful', async () => { createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') }); 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 afde0d9ec4549e92fec30d39d6a8628fc24f8cc3..19b56362ac0a2afb42252188c19e9a6ddf9cace4 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -57,6 +57,15 @@ describe('WorkItemTitle component', () => { }); }); + it('emits updated event', async () => { + createComponent(); + + findItemTitle().vm.$emit('title-changed', 'new title'); + await waitForPromises(); + + expect(wrapper.emitted('updated')).toEqual([[]]); + }); + it('does not call a mutation when the title has not changed', () => { createComponent(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 0c50a3aa50a0bbd1f84dd8cc317bb2d4ee040914..f348355001311eb4fb2fb21d61c05003a0b8a167 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -5,6 +5,7 @@ export const workItemQueryResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Test', state: 'OPEN', + description: 'description', workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -27,6 +28,7 @@ export const updateWorkItemMutationResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Updated title', state: 'OPEN', + description: 'description', workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -65,6 +67,7 @@ export const createWorkItemMutationResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Updated title', state: 'OPEN', + description: 'description', workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js index 39fe7aed0eac257899b8af24ce1b8526b135122c..9f87655175cc99b2f9c2d01c83cfd4262da517ef 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -104,4 +104,18 @@ describe('WorkItemDetail component', () => { issuableId: workItemQueryResponse.data.workItem.id, }); }); + + it('emits workItemUpdated event when fields updated', async () => { + createComponent(); + + await waitForPromises(); + + findWorkItemState().vm.$emit('updated'); + + expect(wrapper.emitted('workItemUpdated')).toEqual([[]]); + + findWorkItemTitle().vm.$emit('updated'); + + expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]); + }); }); 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 81d01a0cb4576a755a597c045b8ffae6a9d73221..85096392e8408ff6504a3326f21ee349844d2ed6 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -1,21 +1,45 @@ +import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { visitUrl } from '~/lib/utils/url_utility'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; +import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql'; +import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); Vue.use(VueApollo); describe('Work items root component', () => { let wrapper; + const issuesListPath = '/-/issues'; + const mockToastShow = jest.fn(); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); + const findAlert = () => wrapper.findComponent(GlAlert); - const createComponent = () => { + const createComponent = ({ + deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse), + } = {}) => { wrapper = shallowMount(WorkItemsRoot, { + apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]), + provide: { + issuesListPath, + }, propsData: { id: '1', }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, }); }; @@ -30,4 +54,34 @@ describe('Work items root component', () => { workItemId: 'gid://gitlab/WorkItem/1', }); }); + + it('deletes work item when deleteWorkItem event emitted', async () => { + const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse); + + createComponent({ + deleteWorkItemHandler, + }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + + await waitForPromises(); + + expect(deleteWorkItemHandler).toHaveBeenCalled(); + expect(mockToastShow).toHaveBeenCalled(); + expect(visitUrl).toHaveBeenCalledWith(issuesListPath); + }); + + it('shows alert if delete fails', async () => { + const deleteWorkItemHandler = jest.fn().mockRejectedValue(deleteWorkItemFailureResponse); + + createComponent({ + deleteWorkItemHandler, + }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + }); }); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 7e68c5e4f0ee3628d41f8d692c62d66abfb6e4d0..99dcd886f7bd2d80acf0668fb44a015777a70490 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -17,6 +17,7 @@ describe('Work items router', () => { router, provide: { fullPath: 'full-path', + issuesListPath: 'full-path/-/issues', }, mocks: { $apollo: {