Skip to content
Snippets Groups Projects
Commit a82efcb5 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo :two:
Browse files

Merge branch '344060-split-create-timeline-event-from-form' into 'master'

Split Create timeline event from Timeline Event form

See merge request !93545
parents 2761b8fc 4e767d1b
No related branches found
No related tags found
2 merge requests!96059Draft: Add GraphQL query for deployment details,!93545Split Create timeline event from Timeline Event form
Pipeline #615364589 passed
import { s__ } from '~/locale'; import { __, s__ } from '~/locale';
export const timelineTabI18n = Object.freeze({ export const timelineTabI18n = Object.freeze({
title: s__('Incident|Timeline'), title: s__('Incident|Timeline'),
...@@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({ ...@@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({
'Incident|Something went wrong while creating the incident timeline event.', 'Incident|Something went wrong while creating the incident timeline event.',
), ),
areaPlaceholder: s__('Incident|Timeline text...'), areaPlaceholder: s__('Incident|Timeline text...'),
save: __('Save'),
cancel: __('Cancel'),
description: __('Description'),
saveAndAdd: s__('Incident|Save and add another event'), saveAndAdd: s__('Incident|Save and add another event'),
areaLabel: s__('Incident|Timeline text'), areaLabel: s__('Incident|Timeline text'),
}); });
......
<script>
import { produce } from 'immer';
import { sortBy } from 'lodash';
import { sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import { timelineFormI18n } from './constants';
import TimelineEventsForm from './timeline_events_form.vue';
import CreateTimelineEvent from './graphql/queries/create_timeline_event.mutation.graphql';
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
export default {
name: 'CreateTimelineEvent',
i18n: timelineFormI18n,
components: {
TimelineEventsForm,
},
inject: ['fullPath', 'issuableId'],
props: {
hasTimelineEvents: {
type: Boolean,
required: true,
},
},
data() {
return { createTimelineEventActive: false };
},
methods: {
clearForm() {
this.$refs.eventForm.clear();
},
focusDate() {
this.$refs.eventForm.focusDate();
},
updateCache(store, { data }) {
const { timelineEvent: event, errors } = data?.timelineEventCreate || {};
if (errors.length) {
return;
}
const variables = {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
fullPath: this.fullPath,
};
const sourceData = store.readQuery({
query: getTimelineEvents,
variables,
});
const newData = produce(sourceData, (draftData) => {
const { nodes: draftEventList } = draftData.project.incidentManagementTimelineEvents;
draftEventList.push(event);
// ISOStrings sort correctly in lexical order
const sortedEvents = sortBy(draftEventList, 'occurredAt');
draftData.project.incidentManagementTimelineEvents.nodes = sortedEvents;
});
store.writeQuery({
query: getTimelineEvents,
variables,
data: newData,
});
},
createIncidentTimelineEvent(eventDetails, addAnotherEvent = false) {
this.createTimelineEventActive = true;
return this.$apollo
.mutate({
mutation: CreateTimelineEvent,
variables: {
input: {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
note: eventDetails.note,
occurredAt: eventDetails.occurredAt,
},
},
update: this.updateCache,
})
.then(({ data = {} }) => {
this.createTimelineEventActive = false;
const errors = data.timelineEventCreate?.errors;
if (errors.length) {
createAlert({
message: sprintf(this.$options.i18n.createError, { error: errors.join('. ') }, false),
});
return;
}
if (addAnotherEvent) {
this.$refs.eventForm.clear();
} else {
this.$emit('hide-new-timeline-events-form');
}
})
.catch((error) => {
createAlert({
message: this.$options.i18n.createErrorGeneric,
captureError: true,
error,
});
});
},
},
};
</script>
<template>
<timeline-events-form
ref="eventForm"
:is-event-processed="createTimelineEventActive"
:has-timeline-events="hasTimelineEvents"
@save-event="createIncidentTimelineEvent"
@cancel="$emit('hide-new-timeline-events-form')"
/>
</template>
<script> <script>
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlIcon } from '@gitlab/ui'; import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlIcon } from '@gitlab/ui';
import { produce } from 'immer';
import { sortBy } from 'lodash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { createAlert } from '~/flash';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { sprintf } from '~/locale';
import { getUtcShiftedDateNow } from './utils';
import { timelineFormI18n } from './constants'; import { timelineFormI18n } from './constants';
import { getUtcShiftedDateNow } from './utils';
import CreateTimelineEvent from './graphql/queries/create_timeline_event.mutation.graphql';
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
export default { export default {
name: 'IncidentTimelineEventForm', name: 'TimelineEventsForm',
restrictedToolBarItems: [ restrictedToolBarItems: [
'quote', 'quote',
'strikethrough', 'strikethrough',
...@@ -38,112 +29,55 @@ export default { ...@@ -38,112 +29,55 @@ export default {
directives: { directives: {
autofocusonshow, autofocusonshow,
}, },
inject: ['fullPath', 'issuableId'],
props: { props: {
hasTimelineEvents: { hasTimelineEvents: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isEventProcessed: {
type: Boolean,
required: true,
},
}, },
data() { data() {
// Create shifted date to force the datepicker to format in UTC // if occurredAt is undefined, returns "now" in UTC
const utcShiftedDate = getUtcShiftedDateNow(); const placeholderDate = getUtcShiftedDateNow();
return { return {
currentDate: utcShiftedDate,
currentHour: utcShiftedDate.getHours(),
currentMinute: utcShiftedDate.getMinutes(),
timelineText: '', timelineText: '',
createTimelineEventActive: false, placeholderDate,
hourPickerInput: placeholderDate.getHours(),
minutePickerInput: placeholderDate.getMinutes(),
datepickerTextInput: null, datepickerTextInput: null,
}; };
}, },
computed: {
occurredAt() {
const [years, months, days] = this.datepickerTextInput.split('-');
const utcDate = new Date(
Date.UTC(years, months - 1, days, this.hourPickerInput, this.minutePickerInput),
);
return utcDate.toISOString();
},
},
methods: { methods: {
clear() { clear() {
const utcShiftedDate = getUtcShiftedDateNow(); const utcShiftedDateNow = getUtcShiftedDateNow();
this.currentDate = utcShiftedDate; this.placeholderDate = utcShiftedDateNow;
this.currentHour = utcShiftedDate.getHours(); this.hourPickerInput = utcShiftedDateNow.getHours();
this.currentMinute = utcShiftedDate.getMinutes(); this.minutePickerInput = utcShiftedDateNow.getMinutes();
}, this.timelineText = '';
hideIncidentTimelineEventForm() {
this.$emit('hide-incident-timeline-event-form');
}, },
focusDate() { focusDate() {
this.$refs.datepicker.$el.focus(); this.$refs.datepicker.$el.focus();
}, },
updateCache(store, { data }) { handleSave(addAnotherEvent) {
const { timelineEvent: event, errors } = data?.timelineEventCreate || {}; const eventDetails = {
note: this.timelineText,
if (errors.length) { occurredAt: this.occurredAt,
return;
}
const variables = {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
fullPath: this.fullPath,
}; };
this.$emit('save-event', eventDetails, addAnotherEvent);
const sourceData = store.readQuery({
query: getTimelineEvents,
variables,
});
const newData = produce(sourceData, (draftData) => {
const { nodes: draftEventList } = draftData.project.incidentManagementTimelineEvents;
draftEventList.push(event);
// ISOStrings sort correctly in lexical order
const sortedEvents = sortBy(draftEventList, 'occurredAt');
draftData.project.incidentManagementTimelineEvents.nodes = sortedEvents;
});
store.writeQuery({
query: getTimelineEvents,
variables,
data: newData,
});
},
createIncidentTimelineEvent(addOneEvent) {
this.createTimelineEventActive = true;
return this.$apollo
.mutate({
mutation: CreateTimelineEvent,
variables: {
input: {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
note: this.timelineText,
occurredAt: this.createDateString(),
},
},
update: this.updateCache,
})
.then(({ data = {} }) => {
const errors = data.timelineEventCreate?.errors;
if (errors.length) {
createAlert({
message: sprintf(this.$options.i18n.createError, { error: errors.join('. ') }, false),
});
}
})
.catch((error) => {
createAlert({
message: this.$options.i18n.createErrorGeneric,
captureError: true,
error,
});
})
.finally(() => {
this.createTimelineEventActive = false;
this.timelineText = '';
if (addOneEvent) {
this.hideIncidentTimelineEventForm();
}
});
},
createDateString() {
const [years, months, days] = this.datepickerTextInput.split('-');
const utcDate = new Date(
Date.UTC(years, months - 1, days, this.currentHour, this.currentMinute),
);
return utcDate.toISOString();
}, },
}, },
}; };
...@@ -165,7 +99,7 @@ export default { ...@@ -165,7 +99,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row datetime-picker" class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row datetime-picker"
> >
<gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5"> <gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5">
<gl-datepicker id="incident-date" #default="{ formattedDate }" v-model="currentDate"> <gl-datepicker id="incident-date" #default="{ formattedDate }" v-model="placeholderDate">
<gl-form-input <gl-form-input
id="incident-date" id="incident-date"
ref="datepicker" ref="datepicker"
...@@ -184,7 +118,7 @@ export default { ...@@ -184,7 +118,7 @@ export default {
<label label-for="timeline-input-hours" class="sr-only"></label> <label label-for="timeline-input-hours" class="sr-only"></label>
<gl-form-input <gl-form-input
id="timeline-input-hours" id="timeline-input-hours"
v-model="currentHour" v-model="hourPickerInput"
data-testid="input-hours" data-testid="input-hours"
size="xs" size="xs"
type="number" type="number"
...@@ -194,7 +128,7 @@ export default { ...@@ -194,7 +128,7 @@ export default {
<label label-for="timeline-input-minutes" class="sr-only"></label> <label label-for="timeline-input-minutes" class="sr-only"></label>
<gl-form-input <gl-form-input
id="timeline-input-minutes" id="timeline-input-minutes"
v-model="currentMinute" v-model="minutePickerInput"
class="gl-ml-3" class="gl-ml-3"
data-testid="input-minutes" data-testid="input-minutes"
size="xs" size="xs"
...@@ -223,9 +157,10 @@ export default { ...@@ -223,9 +157,10 @@ export default {
<textarea <textarea
v-model="timelineText" v-model="timelineText"
class="note-textarea js-gfm-input js-autosize markdown-area" class="note-textarea js-gfm-input js-autosize markdown-area"
data-testid="input-note"
dir="auto" dir="auto"
data-supports-quick-actions="false" data-supports-quick-actions="false"
:aria-label="__('Description')" :aria-label="$options.i18n.description"
:placeholder="$options.i18n.areaPlaceholder" :placeholder="$options.i18n.areaPlaceholder"
> >
</textarea> </textarea>
...@@ -238,26 +173,22 @@ export default { ...@@ -238,26 +173,22 @@ export default {
variant="confirm" variant="confirm"
category="primary" category="primary"
class="gl-mr-3" class="gl-mr-3"
:loading="createTimelineEventActive" :loading="isEventProcessed"
@click="createIncidentTimelineEvent(true)" @click="handleSave(false)"
> >
{{ __('Save') }} {{ $options.i18n.save }}
</gl-button> </gl-button>
<gl-button <gl-button
variant="confirm" variant="confirm"
category="secondary" category="secondary"
class="gl-mr-3 gl-ml-n2" class="gl-mr-3 gl-ml-n2"
:loading="createTimelineEventActive" :loading="isEventProcessed"
@click="createIncidentTimelineEvent(false)" @click="handleSave(true)"
> >
{{ $options.i18n.saveAndAdd }} {{ $options.i18n.saveAndAdd }}
</gl-button> </gl-button>
<gl-button <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
class="gl-ml-n2" {{ $options.i18n.cancel }}
:disabled="createTimelineEventActive"
@click="hideIncidentTimelineEventForm"
>
{{ __('Cancel') }}
</gl-button> </gl-button>
<div class="gl-border-b gl-pt-5"></div> <div class="gl-border-b gl-pt-5"></div>
</gl-form-group> </gl-form-group>
......
...@@ -7,7 +7,7 @@ import getTimelineEvents from './graphql/queries/get_timeline_events.query.graph ...@@ -7,7 +7,7 @@ import getTimelineEvents from './graphql/queries/get_timeline_events.query.graph
import { displayAndLogError } from './utils'; import { displayAndLogError } from './utils';
import { timelineTabI18n } from './constants'; import { timelineTabI18n } from './constants';
import IncidentTimelineEventForm from './timeline_events_form.vue'; import CreateTimelineEvent from './create_timeline_event.vue';
import IncidentTimelineEventsList from './timeline_events_list.vue'; import IncidentTimelineEventsList from './timeline_events_list.vue';
export default { export default {
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlTab, GlTab,
IncidentTimelineEventForm, CreateTimelineEvent,
IncidentTimelineEventsList, IncidentTimelineEventsList,
}, },
i18n: timelineTabI18n, i18n: timelineTabI18n,
...@@ -61,10 +61,10 @@ export default { ...@@ -61,10 +61,10 @@ export default {
this.isEventFormVisible = false; this.isEventFormVisible = false;
}, },
async showEventForm() { async showEventForm() {
this.$refs.eventForm.clear(); this.$refs.createEventForm.clearForm();
this.isEventFormVisible = true; this.isEventFormVisible = true;
await this.$nextTick(); await this.$nextTick();
this.$refs.eventForm.focusDate(); this.$refs.createEventForm.focusDate();
}, },
}, },
}; };
...@@ -82,14 +82,15 @@ export default { ...@@ -82,14 +82,15 @@ export default {
v-if="hasTimelineEvents" v-if="hasTimelineEvents"
:timeline-event-loading="timelineEventLoading" :timeline-event-loading="timelineEventLoading"
:timeline-events="timelineEvents" :timeline-events="timelineEvents"
@hide-new-timeline-events-form="hideEventForm"
/> />
<incident-timeline-event-form <create-timeline-event
v-show="isEventFormVisible" v-show="isEventFormVisible"
ref="eventForm" ref="createEventForm"
:has-timeline-events="hasTimelineEvents" :has-timeline-events="hasTimelineEvents"
class="timeline-event-note timeline-event-note-form" class="timeline-event-note timeline-event-note-form"
:class="{ 'gl-pl-0': !hasTimelineEvents }" :class="{ 'gl-pl-0': !hasTimelineEvents }"
@hide-incident-timeline-event-form="hideEventForm" @hide-new-timeline-events-form="hideEventForm"
/> />
<gl-button v-if="canUpdate" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm"> <gl-button v-if="canUpdate" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm">
{{ $options.i18n.addEventButton }} {{ $options.i18n.addEventButton }}
......
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 createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
import {
timelineEventsCreateEventResponse,
timelineEventsCreateEventError,
mockGetTimelineData,
} from './mock_data';
Vue.use(VueApollo);
jest.mock('~/flash');
const fakeDate = '2020-07-08T00:00:00.000Z';
const mockInputData = {
incidentId: 'gid://gitlab/Issue/1',
note: 'test',
occurredAt: '2020-07-08T00:00:00.000Z',
};
describe('Create Timeline events', () => {
useFakeDate(fakeDate);
let wrapper;
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 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'));
};
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());
};
const fillForm = () => {
setDatetime();
setNoteInput();
};
function createMockApolloProvider() {
const requestHandlers = [[createTimelineEventMutation, responseSpy]];
const mockApollo = createMockApollo(requestHandlers);
mockApollo.clients.defaultClient.cache.writeQuery({
query: getTimelineEvents,
data: mockGetTimelineData,
variables: {
fullPath: 'group/project',
incidentId: 'gid://gitlab/Issue/1',
},
});
return mockApollo;
}
const mountComponent = () => {
wrapper = mountExtended(CreateTimelineEvent, {
propsData: {
hasTimelineEvents: true,
},
provide: {
fullPath: 'group/project',
issuableId: '1',
},
apolloProvider,
});
};
beforeEach(() => {
responseSpy = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse);
apolloProvider = createMockApolloProvider();
});
afterEach(() => {
createAlert.mockReset();
wrapper.destroy();
});
describe('createIncidentTimelineEvent', () => {
const closeFormEvent = { 'hide-new-timeline-events-form': [[]] };
const expectedData = {
input: mockInputData,
};
beforeEach(() => {
mountComponent();
fillForm();
});
describe('on submit', () => {
beforeEach(async () => {
findSubmitButton().trigger('click');
await waitForPromises();
});
it('should call the mutation with the right variables', () => {
expect(responseSpy).toHaveBeenCalledWith(expectedData);
});
it('should close the form on successful addition', () => {
expect(wrapper.emitted()).toEqual(closeFormEvent);
});
});
describe('on submit and add', () => {
beforeEach(async () => {
findSubmitAndAddButton().trigger('click');
await waitForPromises();
});
it('should keep the form open for save and add another', () => {
expect(wrapper.emitted()).toEqual({});
});
});
describe('on cancel', () => {
beforeEach(async () => {
findCancelButton().trigger('click');
await waitForPromises();
});
it('should close the form', () => {
expect(wrapper.emitted()).toEqual(closeFormEvent);
});
});
});
describe('error handling', () => {
it('should show an error when submission returns an error', async () => {
const expectedAlertArgs = {
message: `Error creating incident timeline event: ${timelineEventsCreateEventError.data.timelineEventCreate.errors[0]}`,
};
responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError);
mountComponent();
findSubmitButton().trigger('click');
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 creating the incident timeline event.',
};
responseSpy.mockRejectedValueOnce();
mountComponent();
findSubmitButton().trigger('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
});
it('should keep the form open on failed addition', async () => {
responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError);
mountComponent();
await wrapper.findComponent(TimelineEventsForm).vm.$emit('save-event', mockInputData);
await waitForPromises;
expect(wrapper.emitted()).toEqual({});
});
});
});
...@@ -72,10 +72,14 @@ export const timelineEventsQueryEmptyResponse = { ...@@ -72,10 +72,14 @@ export const timelineEventsQueryEmptyResponse = {
}; };
export const timelineEventsCreateEventResponse = { export const timelineEventsCreateEventResponse = {
timelineEvent: { data: {
...mockEvents[0], timelineEventCreate: {
timelineEvent: {
...mockEvents[0],
},
errors: [],
},
}, },
errors: [],
}; };
export const timelineEventsCreateEventError = { export const timelineEventsCreateEventError = {
...@@ -103,3 +107,21 @@ const timelineEventDeleteData = (errors = []) => { ...@@ -103,3 +107,21 @@ const timelineEventDeleteData = (errors = []) => {
export const timelineEventsDeleteEventResponse = timelineEventDeleteData(); export const timelineEventsDeleteEventResponse = timelineEventDeleteData();
export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']); export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']);
export const mockGetTimelineData = {
project: {
id: 'gid://gitlab/Project/19',
incidentManagementTimelineEvents: {
nodes: [
{
id: 'gid://gitlab/IncidentManagement::TimelineEvent/8',
note: 'another one2',
noteHtml: '<p>another one2</p>',
action: 'comment',
occurredAt: '2022-07-01T12:47:00Z',
createdAt: '2022-07-20T12:47:40Z',
},
],
},
},
};
...@@ -3,49 +3,33 @@ import Vue, { nextTick } from 'vue'; ...@@ -3,49 +3,33 @@ import Vue, { nextTick } from 'vue';
import { GlDatepicker } from '@gitlab/ui'; import { GlDatepicker } from '@gitlab/ui';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.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 createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash'; import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { timelineEventsCreateEventResponse, timelineEventsCreateEventError } from './mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
jest.mock('~/flash'); jest.mock('~/flash');
const addEventResponse = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse); const fakeDate = '2020-07-08T00:00:00.000Z';
function createMockApolloProvider(response = addEventResponse) {
const requestHandlers = [[createTimelineEventMutation, response]];
return createMockApollo(requestHandlers);
}
describe('Timeline events form', () => { describe('Timeline events form', () => {
// July 8 2020 // July 8 2020
useFakeDate(2020, 6, 8); useFakeDate(fakeDate);
let wrapper; let wrapper;
const mountComponent = ({ mockApollo, mountMethod = shallowMountExtended, stubs }) => { const mountComponent = ({ mountMethod = shallowMountExtended }) => {
wrapper = mountMethod(IncidentTimelineEventForm, { wrapper = mountMethod(TimelineEventsForm, {
propsData: { propsData: {
hasTimelineEvents: true, hasTimelineEvents: true,
isEventProcessed: false,
}, },
provide: {
fullPath: 'group/project',
issuableId: '1',
},
apolloProvider: mockApollo,
stubs,
}); });
}; };
afterEach(() => { afterEach(() => {
addEventResponse.mockReset();
createAlert.mockReset(); createAlert.mockReset();
if (wrapper) { wrapper.destroy();
wrapper.destroy();
}
}); });
const findSubmitButton = () => wrapper.findByText('Save'); const findSubmitButton = () => wrapper.findByText('Save');
...@@ -75,24 +59,28 @@ describe('Timeline events form', () => { ...@@ -75,24 +59,28 @@ describe('Timeline events form', () => {
}; };
describe('form button behaviour', () => { describe('form button behaviour', () => {
const closeFormEvent = { 'hide-incident-timeline-event-form': [[]] };
beforeEach(() => { beforeEach(() => {
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended }); mountComponent({ mountMethod: mountExtended });
}); });
it('should close the form on submit', async () => { it('should save event on submit', async () => {
await submitForm(); await submitForm();
expect(wrapper.emitted()).toEqual(closeFormEvent);
expect(wrapper.emitted()).toEqual({
'save-event': [[{ note: '', occurredAt: fakeDate }, false]],
});
}); });
it('should not close the form on "submit and add another"', async () => { it('should save event on "submit and add another"', async () => {
await submitFormAndAddAnother(); await submitFormAndAddAnother();
expect(wrapper.emitted()).toEqual({}); expect(wrapper.emitted()).toEqual({
'save-event': [[{ note: '', occurredAt: fakeDate }, true]],
});
}); });
it('should close the form on cancel', async () => { it('should emit cancel on cancel', async () => {
await cancelForm(); await cancelForm();
expect(wrapper.emitted()).toEqual(closeFormEvent); expect(wrapper.emitted()).toEqual({ cancel: [[]] });
}); });
it('should clear the form', async () => { it('should clear the form', async () => {
...@@ -111,71 +99,4 @@ describe('Timeline events form', () => { ...@@ -111,71 +99,4 @@ describe('Timeline events form', () => {
expect(findMinuteInput().element.value).toBe('0'); expect(findMinuteInput().element.value).toBe('0');
}); });
}); });
describe('addTimelineEventQuery', () => {
const expectedData = {
input: {
incidentId: 'gid://gitlab/Issue/1',
note: '',
occurredAt: '2020-07-08T00:00:00.000Z',
},
};
let mockApollo;
beforeEach(() => {
mockApollo = createMockApolloProvider();
mountComponent({ mockApollo, mountMethod: mountExtended });
});
it('should call the mutation with the right variables', async () => {
await submitForm();
expect(addEventResponse).toHaveBeenCalledWith(expectedData);
});
it('should call the mutation with user selected variables', async () => {
const expectedUserSelectedData = {
input: {
...expectedData.input,
occurredAt: '2021-08-12T05:45:00.000Z',
},
};
setDatetime();
await nextTick();
await submitForm();
expect(addEventResponse).toHaveBeenCalledWith(expectedUserSelectedData);
});
});
describe('error handling', () => {
it('should show an error when submission returns an error', async () => {
const expectedAlertArgs = {
message: 'Error creating incident timeline event: Create error',
};
addEventResponse.mockResolvedValueOnce(timelineEventsCreateEventError);
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
await submitForm();
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 creating the incident timeline event.',
};
addEventResponse.mockRejectedValueOnce();
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
await submitForm();
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
});
});
}); });
...@@ -5,7 +5,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help ...@@ -5,7 +5,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue'; import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue'; import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue';
import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue'; import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql'; import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash'; import { createAlert } from '~/flash';
...@@ -53,7 +53,7 @@ describe('TimelineEventsTab', () => { ...@@ -53,7 +53,7 @@ describe('TimelineEventsTab', () => {
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList); const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
const findTimelineEventForm = () => wrapper.findComponent(IncidentTimelineEventForm); const findCreateTimelineEvent = () => wrapper.findComponent(CreateTimelineEvent);
const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton); const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton);
describe('Timeline events tab', () => { describe('Timeline events tab', () => {
...@@ -143,18 +143,18 @@ describe('TimelineEventsTab', () => { ...@@ -143,18 +143,18 @@ describe('TimelineEventsTab', () => {
}); });
it('should not show a form by default', () => { it('should not show a form by default', () => {
expect(findTimelineEventForm().isVisible()).toBe(false); expect(findCreateTimelineEvent().isVisible()).toBe(false);
}); });
it('should show a form when button is clicked', async () => { it('should show a form when button is clicked', async () => {
await findAddEventButton().trigger('click'); await findAddEventButton().trigger('click');
expect(findTimelineEventForm().isVisible()).toBe(true); expect(findCreateTimelineEvent().isVisible()).toBe(true);
}); });
it('should clear the form when button is clicked', async () => { it('should clear the form when button is clicked', async () => {
const mockClear = jest.fn(); const mockClear = jest.fn();
wrapper.vm.$refs.eventForm.clear = mockClear; wrapper.vm.$refs.createEventForm.clearForm = mockClear;
await findAddEventButton().trigger('click'); await findAddEventButton().trigger('click');
...@@ -165,9 +165,9 @@ describe('TimelineEventsTab', () => { ...@@ -165,9 +165,9 @@ describe('TimelineEventsTab', () => {
// open the form // open the form
await findAddEventButton().trigger('click'); await findAddEventButton().trigger('click');
await findTimelineEventForm().vm.$emit('hide-incident-timeline-event-form'); await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form');
expect(findTimelineEventForm().isVisible()).toBe(false); expect(findCreateTimelineEvent().isVisible()).toBe(false);
}); });
}); });
}); });
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment