diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-20 02:18:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-20 02:18:09 +0300 |
commit | 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch) | |
tree | dc4d20fe6064752c0bd323187252c77e0a89144b /spec/frontend/issues/show | |
parent | 9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff) |
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'spec/frontend/issues/show')
18 files changed, 314 insertions, 212 deletions
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 12f9707da04..3d027e2084c 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -461,7 +461,7 @@ describe('Issuable output', () => { describe('when title is not in view', () => { beforeEach(() => { wrapper.vm.state.titleText = 'Sticky header title'; - wrapper.find(GlIntersectionObserver).vm.$emit('disappear'); + wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); }); it('shows with title', () => { diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index bdb1448148e..9d9abce887b 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -12,6 +12,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createFlash from '~/flash'; import Description from '~/issues/show/components/description.vue'; import { updateHistory } from '~/lib/utils/url_utility'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; @@ -71,7 +72,11 @@ describe('Description component', () => { const findModal = () => wrapper.findComponent(GlModal); const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); - function createComponent({ props = {}, provide } = {}) { + function createComponent({ + props = {}, + provide, + createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler, + } = {}) { wrapper = shallowMountExtended(Description, { propsData: { issueId: 1, @@ -85,7 +90,7 @@ describe('Description component', () => { apolloProvider: createMockApollo([ [workItemQuery, queryHandler], [workItemTypesQuery, workItemTypesQueryHandler], - [createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler], + [createWorkItemFromTaskMutation, createWorkItemFromTaskHandler], ]), mocks: { $toast, @@ -317,7 +322,28 @@ describe('Description component', () => { expect(findModal().exists()).toBe(false); }); + it('shows toast after delete success', async () => { + const newDesc = 'description'; + findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); + + expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); + expect($toast.show).toHaveBeenCalledWith('Task deleted'); + }); + }); + + describe('creating work item from checklist item', () => { it('emits `updateDescription` after creating new work item', async () => { + createComponent({ + props: { + descriptionHtml: descriptionHtmlWithCheckboxes, + }, + provide: { + glFeatures: { + workItemsCreateFromMarkdown: true, + }, + }, + }); + const newDescription = `<p>New description</p>`; await findConvertToTaskButton().trigger('click'); @@ -327,12 +353,28 @@ describe('Description component', () => { expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]); }); - it('shows toast after delete success', async () => { - const newDesc = 'description'; - findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); + it('shows flash message when creating task fails', async () => { + createComponent({ + props: { + descriptionHtml: descriptionHtmlWithCheckboxes, + }, + provide: { + glFeatures: { + workItemsCreateFromMarkdown: true, + }, + }, + createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}), + }); - expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); - expect($toast.show).toHaveBeenCalledWith('Task deleted'); + await findConvertToTaskButton().trigger('click'); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Something went wrong when creating task. Please try again.', + }), + ); }); }); diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js index d58bf1be812..11c43ea4388 100644 --- a/spec/frontend/issues/show/components/edit_actions_spec.js +++ b/spec/frontend/issues/show/components/edit_actions_spec.js @@ -2,16 +2,9 @@ import { GlButton } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; import IssuableEditActions from '~/issues/show/components/edit_actions.vue'; -import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue'; import eventHub from '~/issues/show/event_hub'; -import { - getIssueStateQueryResponse, - updateIssueStateQueryResponse, -} from '../mock_data/apollo_mock'; describe('Edit Actions component', () => { let wrapper; @@ -31,8 +24,6 @@ describe('Edit Actions component', () => { }, }; - const modalId = 'delete-issuable-modal-1'; - const createComponent = ({ props, data } = {}) => { fakeApollo = createMockApollo([], mockResolvers); @@ -50,16 +41,13 @@ describe('Edit Actions component', () => { data() { return { issueState: {}, - modalId, ...data, }; }, }); }; - const findModal = () => wrapper.findComponent(DeleteIssueModal); const findEditButtons = () => wrapper.findAllComponents(GlButton); - const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button'); const findSaveButton = () => wrapper.findByTestId('issuable-save-button'); const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button'); @@ -79,23 +67,12 @@ describe('Edit Actions component', () => { }); }); - it('does not render the delete button if canDestroy is false', () => { - createComponent({ props: { canDestroy: false } }); - expect(findDeleteButton().exists()).toBe(false); - }); - it('disables save button when title is blank', () => { createComponent({ props: { formState: { title: '', issue_type: '' } } }); expect(findSaveButton().attributes('disabled')).toBe('true'); }); - it('does not render the delete button if showDeleteButton is false', () => { - createComponent({ props: { showDeleteButton: false } }); - - expect(findDeleteButton().exists()).toBe(false); - }); - describe('updateIssuable', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); @@ -119,63 +96,4 @@ describe('Edit Actions component', () => { expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); }); - - describe('delete issue button', () => { - let trackingSpy; - - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - it('tracks clicking on button', () => { - findDeleteButton().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'delete_issue', - }); - }); - }); - - describe('delete issue modal', () => { - it('renders', () => { - expect(findModal().props()).toEqual({ - issuePath: 'gitlab-org/gitlab-test/-/issues/1', - issueType: 'Issue', - modalId, - title: 'Delete issue', - }); - }); - }); - - describe('deleteIssuable', () => { - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - - it('does not send the `delete.issuable` event when clicking delete button', () => { - findDeleteButton().vm.$emit('click'); - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); - - it('sends the `delete.issuable` event when clicking the delete confirm button', async () => { - expect(eventHub.$emit).toHaveBeenCalledTimes(0); - findModal().vm.$emit('delete'); - expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable'); - expect(eventHub.$emit).toHaveBeenCalledTimes(1); - }); - }); - - describe('with Apollo cache mock', () => { - it('renders the right delete button text per apollo cache type', async () => { - mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse); - await waitForPromises(); - expect(findDeleteButton().text()).toBe('Delete issue'); - }); - - it('should not change the delete button text per apollo cache mutation', async () => { - mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse); - await waitForPromises(); - expect(findDeleteButton().text()).toBe('Delete issue'); - }); - }); }); diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index d0e33f0b980..61433607a2b 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; describe('Description field component', () => { let wrapper; - const findTextarea = () => wrapper.find({ ref: 'textarea' }); + const findTextarea = () => wrapper.findComponent({ ref: 'textarea' }); const mountComponent = (description = 'test') => shallowMount(DescriptionField, { diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js index de04405d89b..a5fa96d8d64 100644 --- a/spec/frontend/issues/show/components/fields/title_spec.js +++ b/spec/frontend/issues/show/components/fields/title_spec.js @@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub'; describe('Title field component', () => { let wrapper; - const findInput = () => wrapper.find({ ref: 'input' }); + const findInput = () => wrapper.findComponent({ ref: 'input' }); beforeEach(() => { jest.spyOn(eventHub, '$emit'); diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js index 329c4234f30..dc2b3c6fc48 100644 --- a/spec/frontend/issues/show/components/header_actions_spec.js +++ b/spec/frontend/issues/show/components/header_actions_spec.js @@ -65,17 +65,17 @@ describe('HeaderActions component', () => { }, }; - const findToggleIssueStateButton = () => wrapper.find(GlButton); + const findToggleIssueStateButton = () => wrapper.findComponent(GlButton); const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`); const findMobileDropdown = () => findDropdownBy('mobile-dropdown'); const findDesktopDropdown = () => findDropdownBy('desktop-dropdown'); - const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem); - const findDesktopDropdownItems = () => findDesktopDropdown().findAll(GlDropdownItem); + const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem); + const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem); - const findModal = () => wrapper.find(GlModal); + const findModal = () => wrapper.findComponent(GlModal); - const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index); + const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index); const mountComponent = ({ props = {}, diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js index 3ab2bb3460b..1286617d64a 100644 --- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js +++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js @@ -1,13 +1,13 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { GlDatepicker } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue'; import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql'; import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; +import { timelineFormI18n } from '~/issues/show/components/incidents/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createAlert } from '~/flash'; import { useFakeDate } from 'helpers/fake_date'; @@ -35,24 +35,21 @@ describe('Create Timeline events', () => { let responseSpy; let apolloProvider; - const findSubmitButton = () => wrapper.findByText(__('Save')); - const findSubmitAndAddButton = () => - wrapper.findByText(s__('Incident|Save and add another event')); - const findCancelButton = () => wrapper.findByText(__('Cancel')); + const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save); + const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd); + const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel); const findDatePicker = () => wrapper.findComponent(GlDatepicker); const findNoteInput = () => wrapper.findByTestId('input-note'); const setNoteInput = () => { - const textarea = findNoteInput().element; - textarea.value = mockInputData.note; - textarea.dispatchEvent(new Event('input')); + findNoteInput().setValue(mockInputData.note); }; const findHourInput = () => wrapper.findByTestId('input-hours'); const findMinuteInput = () => wrapper.findByTestId('input-minutes'); const setDatetime = () => { const inputDate = new Date(mockInputData.occurredAt); findDatePicker().vm.$emit('input', inputDate); - findHourInput().vm.$emit('input', inputDate.getHours()); - findMinuteInput().vm.$emit('input', inputDate.getMinutes()); + findHourInput().setValue(inputDate.getHours()); + findMinuteInput().setValue(inputDate.getMinutes()); }; const fillForm = () => { setDatetime(); diff --git a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js new file mode 100644 index 00000000000..4c1638a9147 --- /dev/null +++ b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js @@ -0,0 +1,44 @@ +import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; + +import { mockEvents, fakeEventData, mockInputData } from './mock_data'; + +describe('Edit Timeline events', () => { + let wrapper; + + const mountComponent = () => { + wrapper = mountExtended(EditTimelineEvent, { + propsData: { + event: mockEvents[0], + editTimelineEventActive: false, + }, + }); + }; + + beforeEach(() => { + mountComponent(); + }); + + const findTimelineEventsForm = () => wrapper.findComponent(TimelineEventsForm); + + const mockSaveData = { ...fakeEventData, ...mockInputData }; + + describe('editTimelineEvent', () => { + const saveEventEvent = { 'handle-save-edit': [[mockSaveData, false]] }; + + it('should call the mutation with the right variables', async () => { + await findTimelineEventsForm().vm.$emit('save-event', mockSaveData, false); + + expect(wrapper.emitted()).toEqual(saveEventEvent); + }); + + it('should close the form on cancel', async () => { + const cancelEvent = { 'hide-edit': [[]] }; + + await findTimelineEventsForm().vm.$emit('cancel'); + + expect(wrapper.emitted()).toEqual(cancelEvent); + }); + }); +}); diff --git a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js index 155ae703e48..1cfb7d12a91 100644 --- a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js +++ b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js @@ -41,7 +41,7 @@ describe('Highlight Bar', () => { } }); - const findLink = () => wrapper.find(GlLink); + const findLink = () => wrapper.findComponent(GlLink); describe('empty state', () => { beforeEach(() => { diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js index 8e090645be2..d92aeabba0f 100644 --- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js @@ -61,12 +61,12 @@ describe('Incident Tabs component', () => { ); }; - const findTabs = () => wrapper.findAll(GlTab); + const findTabs = () => wrapper.findAllComponents(GlTab); const findSummaryTab = () => findTabs().at(0); const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]'); - const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable); - const findDescriptionComponent = () => wrapper.find(DescriptionComponent); - const findHighlightBarComponent = () => wrapper.find(HighlightBar); + const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable); + const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent); + const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar); const findTimelineTab = () => wrapper.findComponent(TimelineTab); describe('empty state', () => { diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js index 75c0a7350ae..adea2b6df59 100644 --- a/spec/frontend/issues/show/components/incidents/mock_data.js +++ b/spec/frontend/issues/show/components/incidents/mock_data.js @@ -49,6 +49,15 @@ export const mockEvents = [ }, ]; +const mockUpdatedEvent = { + id: 'gid://gitlab/IncidentManagement::TimelineEvent/8', + note: 'another one23', + noteHtml: '<p>another one23</p>', + action: 'comment', + occurredAt: '2022-07-01T12:47:00Z', + createdAt: '2022-07-20T12:47:40Z', +}; + export const timelineEventsQueryListResponse = { data: { project: { @@ -93,6 +102,29 @@ export const timelineEventsCreateEventError = { }, }; +export const timelineEventsEditEventResponse = { + data: { + timelineEventUpdate: { + timelineEvent: { + ...mockUpdatedEvent, + }, + errors: [], + __typename: 'TimelineEventUpdatePayload', + }, + }, +}; + +export const timelineEventsEditEventError = { + data: { + timelineEventUpdate: { + timelineEvent: { + ...mockUpdatedEvent, + }, + errors: ['Create error'], + }, + }, +}; + const timelineEventDeleteData = (errors = []) => { return { data: { @@ -125,3 +157,13 @@ export const mockGetTimelineData = { }, }, }; + +export const fakeDate = '2020-07-08T00:00:00.000Z'; + +export const mockInputData = { + note: 'test', + occurredAt: '2020-08-10T02:30:00.000Z', +}; + +const { id, note, occurredAt } = mockEvents[0]; +export const fakeEventData = { id, note, occurredAt }; diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js index cd2cbb63246..7f086a276f7 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js @@ -4,6 +4,8 @@ import { GlDatepicker } from '@gitlab/ui'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { timelineFormI18n } from '~/issues/show/components/incidents/constants'; import { createAlert } from '~/flash'; import { useFakeDate } from 'helpers/fake_date'; @@ -13,6 +15,8 @@ jest.mock('~/flash'); const fakeDate = '2020-07-08T00:00:00.000Z'; +const mockInputDate = new Date('2021-08-12'); + describe('Timeline events form', () => { // July 8 2020 useFakeDate(fakeDate); @@ -21,7 +25,7 @@ describe('Timeline events form', () => { const mountComponent = ({ mountMethod = shallowMountExtended }) => { wrapper = mountMethod(TimelineEventsForm, { propsData: { - hasTimelineEvents: true, + showSaveAndAdd: true, isEventProcessed: false, }, }); @@ -32,17 +36,17 @@ describe('Timeline events form', () => { wrapper.destroy(); }); - const findSubmitButton = () => wrapper.findByText('Save'); - const findSubmitAndAddButton = () => wrapper.findByText('Save and add another event'); - const findCancelButton = () => wrapper.findByText('Cancel'); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); + const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save); + const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd); + const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel); const findDatePicker = () => wrapper.findComponent(GlDatepicker); - const findDatePickerInput = () => wrapper.findByTestId('input-datepicker'); const findHourInput = () => wrapper.findByTestId('input-hours'); const findMinuteInput = () => wrapper.findByTestId('input-minutes'); const setDatetime = () => { - findDatePicker().vm.$emit('input', new Date('2021-08-12')); - findHourInput().vm.$emit('input', 5); - findMinuteInput().vm.$emit('input', 45); + findDatePicker().vm.$emit('input', mockInputDate); + findHourInput().setValue(5); + findMinuteInput().setValue(45); }; const submitForm = async () => { @@ -58,6 +62,22 @@ describe('Timeline events form', () => { await waitForPromises(); }; + it('renders markdown-field component with correct list of toolbar items', () => { + mountComponent({ mountMethod: mountExtended }); + + expect(findMarkdownField().props('restrictedToolBarItems')).toEqual([ + 'quote', + 'strikethrough', + 'bullet-list', + 'numbered-list', + 'task-list', + 'collapsible-section', + 'table', + 'attach-file', + 'full-screen', + ]); + }); + describe('form button behaviour', () => { beforeEach(() => { mountComponent({ mountMethod: mountExtended }); @@ -87,14 +107,14 @@ describe('Timeline events form', () => { setDatetime(); await nextTick(); - expect(findDatePickerInput().element.value).toBe('2021-08-12'); + expect(findDatePicker().props('value')).toBe(mockInputDate); expect(findHourInput().element.value).toBe('5'); expect(findMinuteInput().element.value).toBe('45'); wrapper.vm.clear(); await nextTick(); - expect(findDatePickerInput().element.value).toBe('2020-07-08'); + expect(findDatePicker().props('value')).toStrictEqual(new Date(fakeDate)); expect(findHourInput().element.value).toBe('0'); expect(findMinuteInput().element.value).toBe('0'); }); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js index 90e55003ab3..1bf8d68efd4 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js @@ -1,6 +1,7 @@ import timezoneMock from 'timezone-mock'; import { GlIcon, GlDropdown } from '@gitlab/ui'; import { nextTick } from 'vue'; +import { timelineItemI18n } from '~/issues/show/components/incidents/constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue'; import { mockEvents } from './mock_data'; @@ -15,21 +16,19 @@ describe('IncidentTimelineEventList', () => { action, noteHtml, occurredAt, - isLastItem: false, ...propsData, }, provide: { - canUpdate: false, + canUpdateTimelineEvent: false, ...provide, }, }); }; const findCommentIcon = () => wrapper.findComponent(GlIcon); - const findTextContainer = () => wrapper.findByTestId('event-text-container'); const findEventTime = () => wrapper.findByTestId('event-time'); const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDeleteButton = () => wrapper.findByText('Delete'); + const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete); describe('template', () => { it('shows comment icon', () => { @@ -50,20 +49,6 @@ describe('IncidentTimelineEventList', () => { expect(findEventTime().text()).toBe('15:59 UTC'); }); - describe('last item in list', () => { - it('shows a bottom border when not the last item', () => { - mountComponent(); - - expect(findTextContainer().classes()).toContain('gl-border-1'); - }); - - it('does not show a bottom border when the last item', () => { - mountComponent({ propsData: { isLastItem: true } }); - - expect(wrapper.classes()).not.toContain('gl-border-1'); - }); - }); - describe.each` timezone ${'Europe/London'} @@ -96,20 +81,20 @@ describe('IncidentTimelineEventList', () => { }); it('shows dropdown and delete item when user has update permission', () => { - mountComponent({ provide: { canUpdate: true } }); + mountComponent({ provide: { canUpdateTimelineEvent: true } }); expect(findDropdown().exists()).toBe(true); expect(findDeleteButton().exists()).toBe(true); }); it('triggers a delete when the delete button is clicked', async () => { - mountComponent({ provide: { canUpdate: true } }); + mountComponent({ provide: { canUpdateTimelineEvent: true } }); findDeleteButton().trigger('click'); await nextTick(); - expect(wrapper.emitted().delete).toBeTruthy(); + expect(wrapper.emitted().delete).toHaveLength(1); }); }); }); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js index 4d2d53c990e..dff1c429d07 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js @@ -3,16 +3,24 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue'; -import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_item.vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue'; +import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql'; +import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { useFakeDate } from 'helpers/fake_date'; import { createAlert } from '~/flash'; import { mockEvents, timelineEventsDeleteEventResponse, timelineEventsDeleteEventError, + timelineEventsEditEventResponse, + timelineEventsEditEventError, + fakeDate, + fakeEventData, + mockInputData, } from './mock_data'; Vue.use(VueApollo); @@ -20,83 +28,73 @@ Vue.use(VueApollo); jest.mock('~/flash'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); -const deleteEventResponse = jest.fn(); - -function createMockApolloProvider() { - deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventResponse); - const requestHandlers = [[deleteTimelineEventMutation, deleteEventResponse]]; - return createMockApollo(requestHandlers); -} - const mockConfirmAction = ({ confirmed }) => { confirmAction.mockResolvedValueOnce(confirmed); }; describe('IncidentTimelineEventList', () => { + useFakeDate(fakeDate); let wrapper; + const deleteResponseSpy = jest.fn().mockResolvedValue(timelineEventsDeleteEventResponse); + const editResponseSpy = jest.fn().mockResolvedValue(timelineEventsEditEventResponse); - const mountComponent = (mockApollo) => { - const apollo = mockApollo ? { apolloProvider: mockApollo } : {}; + const requestHandlers = [ + [deleteTimelineEventMutation, deleteResponseSpy], + [editTimelineEventMutation, editResponseSpy], + ]; + const apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMountExtended(IncidentTimelineEventList, { + const mountComponent = () => { + wrapper = mountExtended(IncidentTimelineEventList, { + propsData: { + timelineEvents: mockEvents, + }, provide: { fullPath: 'group/project', issuableId: '1', + canUpdateTimelineEvent: true, }, - propsData: { - timelineEvents: mockEvents, - }, - ...apollo, + apolloProvider, }); }; const findTimelineEventGroups = () => wrapper.findAllByTestId('timeline-group'); - const findItems = (base = wrapper) => base.findAll(IncidentTimelineEventListItem); + const findItems = (base = wrapper) => base.findAllComponents(IncidentTimelineEventItem); const findFirstTimelineEventGroup = () => findTimelineEventGroups().at(0); const findSecondTimelineEventGroup = () => findTimelineEventGroups().at(1); const findDates = () => wrapper.findAllByTestId('event-date'); const clickFirstDeleteButton = async () => { - findItems() - .at(0) - .vm.$emit('delete', { ...mockEvents[0] }); + findItems().at(0).vm.$emit('delete', { fakeEventData }); await waitForPromises(); }; + const clickFirstEditButton = async () => { + findItems().at(0).vm.$emit('edit'); + await waitForPromises(); + }; + beforeEach(() => { + mountComponent(); + }); + afterEach(() => { - confirmAction.mockReset(); - deleteEventResponse.mockReset(); wrapper.destroy(); }); describe('template', () => { it('groups items correctly', () => { - mountComponent(); - expect(findTimelineEventGroups()).toHaveLength(2); expect(findItems(findFirstTimelineEventGroup())).toHaveLength(1); expect(findItems(findSecondTimelineEventGroup())).toHaveLength(2); }); - it('sets the isLastItem prop correctly', () => { - mountComponent(); - - expect(findItems().at(0).props('isLastItem')).toBe(false); - expect(findItems().at(1).props('isLastItem')).toBe(false); - expect(findItems().at(2).props('isLastItem')).toBe(true); - }); - it('sets the event props correctly', () => { - mountComponent(); - expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt); expect(findItems().at(1).props('action')).toBe(mockEvents[1].action); expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml); }); it('formats dates correctly', () => { - mountComponent(); - expect(findDates().at(0).text()).toBe('2022-03-22'); expect(findDates().at(1).text()).toBe('2022-03-23'); }); @@ -110,8 +108,6 @@ describe('IncidentTimelineEventList', () => { describe(timezone, () => { beforeEach(() => { timezoneMock.register(timezone); - - mountComponent(); }); afterEach(() => { @@ -131,12 +127,9 @@ describe('IncidentTimelineEventList', () => { it('should delete when button is clicked', async () => { const expectedVars = { input: { id: mockEvents[0].id } }; - - mountComponent(createMockApolloProvider()); - await clickFirstDeleteButton(); - expect(deleteEventResponse).toHaveBeenCalledWith(expectedVars); + expect(deleteResponseSpy).toHaveBeenCalledWith(expectedVars); }); it('should show an error when delete returns an error', async () => { @@ -144,8 +137,7 @@ describe('IncidentTimelineEventList', () => { message: 'Error deleting incident timeline event: Item does not exist', }; - mountComponent(createMockApolloProvider()); - deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventError); + deleteResponseSpy.mockResolvedValue(timelineEventsDeleteEventError); await clickFirstDeleteButton(); @@ -158,8 +150,7 @@ describe('IncidentTimelineEventList', () => { error: new Error(), message: 'Something went wrong while deleting the incident timeline event.', }; - mountComponent(createMockApolloProvider()); - deleteEventResponse.mockRejectedValueOnce(); + deleteResponseSpy.mockRejectedValueOnce(); await clickFirstDeleteButton(); @@ -167,4 +158,76 @@ describe('IncidentTimelineEventList', () => { }); }); }); + + describe('Edit Functionality', () => { + beforeEach(() => { + mountComponent(); + clickFirstEditButton(); + }); + + const findEditEvent = () => wrapper.findComponent(EditTimelineEvent); + const mockSaveData = { ...fakeEventData, ...mockInputData }; + + describe('editTimelineEvent', () => { + it('should call the mutation with the right variables', async () => { + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(editResponseSpy).toHaveBeenCalledWith({ + input: mockSaveData, + }); + }); + + it('should close the form on successful addition', async () => { + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(findEditEvent().exists()).toBe(false); + }); + + it('should close the form on cancel', async () => { + await findEditEvent().vm.$emit('hide-edit'); + await waitForPromises(); + + expect(findEditEvent().exists()).toBe(false); + }); + }); + + describe('error handling', () => { + it('should show an error when submission returns an error', async () => { + const expectedAlertArgs = { + message: `Error updating incident timeline event: ${timelineEventsEditEventError.data.timelineEventUpdate.errors[0]}`, + }; + editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError); + + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); + }); + + it('should show an error when submission fails', async () => { + const expectedAlertArgs = { + captureError: true, + error: new Error(), + message: 'Something went wrong while updating the incident timeline event.', + }; + editResponseSpy.mockRejectedValueOnce(); + + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); + }); + + it('should keep the form open on failed addition', async () => { + editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError); + + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(findEditEvent().exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js index 2cdb971395d..5bac1d6e7ad 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js @@ -36,7 +36,7 @@ describe('TimelineEventsTab', () => { provide: { fullPath: 'group/project', issuableId: '1', - canUpdate: true, + canUpdateTimelineEvent: true, ...provide, }, apolloProvider: mockApollo, @@ -136,29 +136,20 @@ describe('TimelineEventsTab', () => { it('should not show a button when user cannot update', () => { mountComponent({ mockApollo: createMockApolloProvider(emptyResponse), - provide: { canUpdate: false }, + provide: { canUpdateTimelineEvent: false }, }); expect(findAddEventButton().exists()).toBe(false); }); it('should not show a form by default', () => { - expect(findCreateTimelineEvent().isVisible()).toBe(false); + expect(findCreateTimelineEvent().exists()).toBe(false); }); it('should show a form when button is clicked', async () => { await findAddEventButton().trigger('click'); - expect(findCreateTimelineEvent().isVisible()).toBe(true); - }); - - it('should clear the form when button is clicked', async () => { - const mockClear = jest.fn(); - wrapper.vm.$refs.createEventForm.clearForm = mockClear; - - await findAddEventButton().trigger('click'); - - expect(mockClear).toHaveBeenCalled(); + expect(findCreateTimelineEvent().exists()).toBe(true); }); it('should hide the form when the hide event is emitted', async () => { @@ -167,7 +158,7 @@ describe('TimelineEventsTab', () => { await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form'); - expect(findCreateTimelineEvent().isVisible()).toBe(false); + expect(findCreateTimelineEvent().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js index d3a86680f14..f0494591e95 100644 --- a/spec/frontend/issues/show/components/incidents/utils_spec.js +++ b/spec/frontend/issues/show/components/incidents/utils_spec.js @@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock'; import { displayAndLogError, getEventIcon, - getUtcShiftedDateNow, + getUtcShiftedDate, } from '~/issues/show/components/incidents/utils'; import { createAlert } from '~/flash'; @@ -34,7 +34,7 @@ describe('incident utils', () => { }); }); - describe('getUtcShiftedDateNow', () => { + describe('getUtcShiftedDate', () => { beforeEach(() => { timezoneMock.register('US/Pacific'); }); @@ -46,7 +46,7 @@ describe('incident utils', () => { it('should shift the date by the timezone offset', () => { const date = new Date(); - const shiftedDate = getUtcShiftedDateNow(); + const shiftedDate = getUtcShiftedDate(); expect(shiftedDate > date).toBe(true); }); diff --git a/spec/frontend/issues/show/components/pinned_links_spec.js b/spec/frontend/issues/show/components/pinned_links_spec.js index aac720df6e9..208baac7124 100644 --- a/spec/frontend/issues/show/components/pinned_links_spec.js +++ b/spec/frontend/issues/show/components/pinned_links_spec.js @@ -9,7 +9,7 @@ const plainStatusUrl = 'https://status.com'; describe('PinnedLinks', () => { let wrapper; - const findButtons = () => wrapper.findAll(GlButton); + const findButtons = () => wrapper.findAllComponents(GlButton); const createComponent = (props) => { wrapper = shallowMount(PinnedLinks, { diff --git a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js index b38d2b60057..d4202f4a6ab 100644 --- a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js +++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js @@ -62,8 +62,8 @@ describe('Sentry Error Stack Trace', () => { describe('loading', () => { it('should show spinner while loading', () => { mountComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find(Stacktrace).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(false); }); }); @@ -74,8 +74,8 @@ describe('Sentry Error Stack Trace', () => { it('should show stacktrace', () => { mountComponent({ stubs: {} }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(Stacktrace).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(true); }); }); }); |