diff options
Diffstat (limited to 'spec/frontend/work_items/components/work_item_notes_spec.js')
-rw-r--r-- | spec/frontend/work_items/components/work_item_notes_spec.js | 171 |
1 files changed, 162 insertions, 9 deletions
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index 23dd2b6bacb..3db848a0ad2 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -1,22 +1,26 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlSkeletonLoader, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import SystemNote from '~/work_items/components/notes/system_note.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; -import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue'; +import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; +import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue'; import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; -import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql'; -import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql'; +import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql'; +import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; +import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql'; import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants'; -import { DESC } from '~/notes/constants'; +import { ASC, DESC } from '~/notes/constants'; import { mockWorkItemNotesResponse, workItemQueryResponse, mockWorkItemNotesByIidResponse, mockMoreWorkItemNotesResponse, + mockWorkItemNotesResponseWithComments, } from '../mock_data'; const mockWorkItemId = workItemQueryResponse.data.workItem.id; @@ -32,34 +36,56 @@ const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem. (widget) => widget.type === WIDGET_TYPE_NOTES, ); +const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_NOTES, +); + const firstSystemNodeId = mockNotesWidgetResponse.discussions.nodes[0].notes.nodes[0].id; +const mockDiscussions = mockWorkItemNotesWidgetResponseWithComments.discussions.nodes; + describe('WorkItemNotes component', () => { let wrapper; Vue.use(VueApollo); + const showModal = jest.fn(); + const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote); + const findAllListItems = () => wrapper.findAll('ul.timeline > *'); const findActivityLabel = () => wrapper.find('label'); - const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm); + const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findSortingFilter = () => wrapper.findComponent(ActivityFilter); const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index); + const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion); + const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index); + const findDeleteNoteModal = () => wrapper.findComponent(GlModal); + const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse); const workItemNotesByIidQueryHandler = jest .fn() .mockResolvedValue(mockWorkItemNotesByIidResponse); const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse); + const workItemNotesWithCommentsQueryHandler = jest + .fn() + .mockResolvedValue(mockWorkItemNotesResponseWithComments); + const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({ + data: { destroyNote: { note: null, __typename: 'DestroyNote' } }, + }); + const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false, defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler, + deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler, } = {}) => { wrapper = shallowMount(WorkItemNotes, { apolloProvider: createMockApollo([ [workItemNotesQuery, defaultWorkItemNotesQueryHandler], [workItemNotesByIidQuery, workItemNotesByIidQueryHandler], + [deleteWorkItemNoteMutation, deleteWINoteMutationHandler], ]), propsData: { workItemId, @@ -75,6 +101,9 @@ describe('WorkItemNotes component', () => { useIidInWorkItemsPath: fetchByIid, }, }, + stubs: { + GlModal: stubComponent(GlModal, { methods: { show: showModal } }), + }, }); }; @@ -87,10 +116,14 @@ describe('WorkItemNotes component', () => { }); it('passes correct props to comment form component', async () => { - createComponent({ workItemId: mockWorkItemId, fetchByIid: false }); + createComponent({ + workItemId: mockWorkItemId, + fetchByIid: false, + defaultWorkItemNotesQueryHandler: workItemNotesByIidQueryHandler, + }); await waitForPromises(); - expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false); + expect(findWorkItemAddNote().props('fetchByIid')).toEqual(false); }); describe('when notes are loading', () => { @@ -121,13 +154,14 @@ describe('WorkItemNotes component', () => { }); it('renders the notes list to the length of the response', () => { + expect(workItemNotesByIidQueryHandler).toHaveBeenCalled(); expect(findAllSystemNotes()).toHaveLength( mockNotesByIidWidgetResponse.discussions.nodes.length, ); }); it('passes correct props to comment form component', () => { - expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true); + expect(findWorkItemAddNote().props('fetchByIid')).toEqual(true); }); }); @@ -180,5 +214,124 @@ describe('WorkItemNotes component', () => { expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId); }); + + it('puts form at start of list in when sorting by newest first', async () => { + await findSortingFilter().vm.$emit('changeSortOrder', DESC); + + expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true); + }); + + it('puts form at end of list in when sorting by oldest first', async () => { + await findSortingFilter().vm.$emit('changeSortOrder', ASC); + + expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true); + }); + }); + + describe('Activity comments', () => { + beforeEach(async () => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + await waitForPromises(); + }); + + it('should not have any system notes', () => { + expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled(); + expect(findAllSystemNotes()).toHaveLength(0); + }); + + it('should have work item notes', () => { + expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled(); + expect(findAllWorkItemCommentNotes()).toHaveLength(mockDiscussions.length); + }); + + it('should pass all the correct props to work item comment note', () => { + const commentIndex = 0; + const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex); + + expect(firstCommentNote.props('discussion')).toEqual( + mockDiscussions[commentIndex].notes.nodes, + ); + }); + }); + + it('should open delete modal confirmation when child discussion emits `deleteNote` event', async () => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + await waitForPromises(); + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false }); + expect(showModal).toHaveBeenCalled(); + }); + + describe('when modal is open', () => { + beforeEach(() => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + return waitForPromises(); + }); + + it('sends the mutation with correct variables', () => { + const noteId = 'some-test-id'; + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId }); + findDeleteNoteModal().vm.$emit('primary'); + + expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: noteId, + }, + }); + }); + + it('successfully removes the note from the discussion', async () => { + expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2); + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { + id: mockDiscussions[0].notes.nodes[0].id, + }); + findDeleteNoteModal().vm.$emit('primary'); + + await waitForPromises(); + expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1); + }); + + it('successfully removes the discussion from work item if discussion only had one note', async () => { + const secondDiscussion = findWorkItemCommentNoteAtIndex(1); + + expect(findAllWorkItemCommentNotes()).toHaveLength(2); + expect(secondDiscussion.props('discussion')).toHaveLength(1); + + secondDiscussion.vm.$emit('deleteNote', { + id: mockDiscussions[1].notes.nodes[0].id, + discussion: { id: mockDiscussions[1].id }, + }); + findDeleteNoteModal().vm.$emit('primary'); + + await waitForPromises(); + expect(findAllWorkItemCommentNotes()).toHaveLength(1); + }); + }); + + it('emits `error` event if delete note mutation is rejected', async () => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + deleteWINoteMutationHandler: errorHandler, + }); + await waitForPromises(); + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { + id: mockDiscussions[0].notes.nodes[0].id, + }); + findDeleteNoteModal().vm.$emit('primary'); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong when deleting a comment. Please try again'], + ]); }); }); |