Commit 6d7bb5d2 authored by Coung Ngo's avatar Coung Ngo Committed by Natalia Tepluhina

Edit title in work item detail modal

Behind `work_items` feature flag, defaulted off
parent f1a210d0
...@@ -239,9 +239,6 @@ export default { ...@@ -239,9 +239,6 @@ export default {
closeWorkItemDetailModal() { closeWorkItemDetailModal() {
this.workItemId = null; this.workItemId = null;
}, },
handleWorkItemDetailModalError(message) {
createFlash({ message });
},
handleCreateTask(description) { handleCreateTask(description) {
this.$emit('updateDescription', description); this.$emit('updateDescription', description);
this.closeCreateTaskModal(); this.closeCreateTaskModal();
...@@ -306,7 +303,6 @@ export default { ...@@ -306,7 +303,6 @@ export default {
:visible="showWorkItemDetailModal" :visible="showWorkItemDetailModal"
:work-item-id="workItemId" :work-item-id="workItemId"
@close="closeWorkItemDetailModal" @close="closeWorkItemDetailModal"
@error="handleWorkItemDetailModalError"
/> />
<template v-if="workItemsEnabled"> <template v-if="workItemsEnabled">
<gl-popover <gl-popover
......
...@@ -2,10 +2,7 @@ ...@@ -2,10 +2,7 @@
import { escape } from 'lodash'; import { escape } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { WI_TITLE_TRACK_LABEL } from '../constants';
export default { export default {
WI_TITLE_TRACK_LABEL,
props: { props: {
initialTitle: { initialTitle: {
type: String, type: String,
...@@ -50,7 +47,6 @@ export default { ...@@ -50,7 +47,6 @@ export default {
<h2 <h2
class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block" class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block"
:class="{ 'gl-cursor-not-allowed': disabled }" :class="{ 'gl-cursor-not-allowed': disabled }"
data-testid="title"
aria-labelledby="item-title" aria-labelledby="item-title"
> >
<span <span
...@@ -59,7 +55,6 @@ export default { ...@@ -59,7 +55,6 @@ export default {
role="textbox" role="textbox"
:aria-label="__('Title')" :aria-label="__('Title')"
:data-placeholder="placeholder" :data-placeholder="placeholder"
:data-track-label="$options.WI_TITLE_TRACK_LABEL"
:contenteditable="!disabled" :contenteditable="!disabled"
class="gl-pseudo-placeholder" class="gl-pseudo-placeholder"
@blur="handleBlur" @blur="handleBlur"
......
<script> <script>
import { GlModal, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale'; import { i18n } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql'; import workItemQuery from '../graphql/work_item.query.graphql';
import ItemTitle from './item_title.vue'; import WorkItemTitle from './work_item_title.vue';
export default { export default {
i18n,
components: { components: {
GlAlert,
GlModal, GlModal,
GlLoadingIcon, WorkItemTitle,
ItemTitle,
}, },
props: { props: {
visible: { visible: {
...@@ -23,6 +24,7 @@ export default { ...@@ -23,6 +24,7 @@ export default {
}, },
data() { data() {
return { return {
error: undefined,
workItem: {}, workItem: {},
}; };
}, },
...@@ -34,23 +36,17 @@ export default { ...@@ -34,23 +36,17 @@ export default {
id: this.workItemId, id: this.workItemId,
}; };
}, },
update(data) {
return data.workItem;
},
skip() { skip() {
return !this.workItemId; return !this.workItemId;
}, },
error() { error() {
this.$emit( this.error = this.$options.i18n.fetchError;
'error',
s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
);
}, },
}, },
}, },
computed: { computed: {
workItemTitle() { workItemType() {
return this.workItem?.title; return this.workItem.workItemType?.name;
}, },
}, },
}; };
...@@ -58,7 +54,16 @@ export default { ...@@ -58,7 +54,16 @@ export default {
<template> <template>
<gl-modal hide-footer modal-id="work-item-detail-modal" :visible="visible" @hide="$emit('close')"> <gl-modal hide-footer modal-id="work-item-detail-modal" :visible="visible" @hide="$emit('close')">
<gl-loading-icon v-if="$apollo.queries.workItem.loading" size="md" /> <gl-alert v-if="error" variant="danger" @dismiss="error = false">
<item-title v-else class="gl-m-0!" :initial-title="workItemTitle" /> {{ error }}
</gl-alert>
<work-item-title
:loading="$apollo.queries.workItem.loading"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
@error="error = $event"
/>
</gl-modal> </gl-modal>
</template> </template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Tracking from '~/tracking';
import { i18n } from '../constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import ItemTitle from './item_title.vue';
export default {
components: {
GlLoadingIcon,
ItemTitle,
},
mixins: [Tracking.mixin()],
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
workItemId: {
type: String,
required: false,
default: '',
},
workItemTitle: {
type: String,
required: false,
default: '',
},
workItemType: {
type: String,
required: false,
default: '',
},
},
computed: {
tracking() {
return {
category: 'workItems:show',
label: 'item_title',
property: `type_${this.workItemType}`,
};
},
},
methods: {
async updateWorkItem(updatedTitle) {
if (updatedTitle === this.workItemTitle) {
return;
}
try {
await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
title: updatedTitle,
},
},
});
this.track('updated_title');
} catch {
this.$emit('error', i18n.updateError);
}
},
},
};
</script>
<template>
<gl-loading-icon v-if="loading" class="gl-mt-3" size="md" />
<item-title v-else :initial-title="workItemTitle" @title-changed="updateWorkItem" />
</template>
import { s__ } from '~/locale';
export const i18n = {
fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
};
export const widgetTypes = { export const widgetTypes = {
title: 'TITLE', title: 'TITLE',
}; };
export const WI_TITLE_TRACK_LABEL = 'item_title';
#import './widget.fragment.graphql' #import "./work_item.fragment.graphql"
mutation createWorkItem($input: WorkItemCreateInput!) { mutation createWorkItem($input: WorkItemCreateInput!) {
workItemCreate(input: $input) { workItemCreate(input: $input) {
workItem { workItem {
id ...WorkItem
title
workItemType {
id
}
widgets @client {
nodes {
...WidgetBase
}
}
} }
} }
} }
...@@ -23,12 +23,16 @@ export function createApolloProvider() { ...@@ -23,12 +23,16 @@ export function createApolloProvider() {
id: 'gid://gitlab/WorkItem/1', id: 'gid://gitlab/WorkItem/1',
}, },
data: { data: {
localWorkItem: { workItem: {
__typename: 'LocalWorkItem', __typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1', id: 'gid://gitlab/WorkItem/1',
type: 'FEATURE',
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
title: 'Test Work Item', title: 'Test Work Item',
workItemType: {
__typename: 'WorkItemType',
id: 'work-item-type-1',
name: 'Type', // eslint-disable-line @gitlab/require-i18n-strings
},
widgets: { widgets: {
__typename: 'LocalWorkItemWidgetConnection', __typename: 'LocalWorkItemWidgetConnection',
nodes: [], nodes: [],
......
#import './widget.fragment.graphql' #import "./work_item.fragment.graphql"
mutation workItemUpdate($input: WorkItemUpdateInput!) { mutation workItemUpdate($input: WorkItemUpdateInput!) {
workItemUpdate(input: $input) { workItemUpdate(input: $input) {
workItem { workItem {
id ...WorkItem
title
workItemType {
id
}
widgets @client {
nodes {
...WidgetBase
}
}
} }
} }
} }
fragment WorkItem on WorkItem {
id
title
workItemType {
id
name
}
}
#import './widget.fragment.graphql' #import "./work_item.fragment.graphql"
query WorkItem($id: ID!) { query workItem($id: ID!) {
workItem(id: $id) { workItem(id: $id) {
id ...WorkItem
title
workItemType {
id
}
widgets @client {
nodes {
...WidgetBase
}
}
} }
} }
<script> <script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking'; import { i18n } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql'; import workItemQuery from '../graphql/work_item.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import WorkItemTitle from '../components/work_item_title.vue';
import { WI_TITLE_TRACK_LABEL } from '../constants';
import ItemTitle from '../components/item_title.vue';
const trackingMixin = Tracking.mixin();
export default { export default {
titleUpdatedEvent: 'updated_title', i18n,
components: { components: {
ItemTitle,
GlAlert, GlAlert,
GlLoadingIcon, WorkItemTitle,
}, },
mixins: [trackingMixin],
props: { props: {
id: { id: {
type: String, type: String,
...@@ -27,7 +21,7 @@ export default { ...@@ -27,7 +21,7 @@ export default {
data() { data() {
return { return {
workItem: {}, workItem: {},
error: false, error: undefined,
}; };
}, },
apollo: { apollo: {
...@@ -38,37 +32,17 @@ export default { ...@@ -38,37 +32,17 @@ export default {
id: this.gid, id: this.gid,
}; };
}, },
error() {
this.error = this.$options.i18n.fetchError;
},
}, },
}, },
computed: { computed: {
tracking() {
return {
category: 'workItems:show',
action: 'updated_title',
label: WI_TITLE_TRACK_LABEL,
property: '[type_work_item]',
};
},
gid() { gid() {
return convertToGraphQLId('WorkItem', this.id); return convertToGraphQLId(TYPE_WORK_ITEM, this.id);
}, },
}, workItemType() {
methods: { return this.workItem.workItemType?.name;
async updateWorkItem(updatedTitle) {
try {
await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.gid,
title: updatedTitle,
},
},
});
this.track();
} catch {
this.error = true;
}
}, },
}, },
}; };
...@@ -76,23 +50,16 @@ export default { ...@@ -76,23 +50,16 @@ export default {
<template> <template>
<section> <section>
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{ <gl-alert v-if="error" variant="danger" @dismiss="error = false">
__('Something went wrong while updating work item. Please try again') {{ error }}
}}</gl-alert> </gl-alert>
<!-- Title widget placeholder -->
<div> <work-item-title
<gl-loading-icon :loading="$apollo.queries.workItem.loading"
v-if="$apollo.queries.workItem.loading" :work-item-id="workItem.id"
size="md" :work-item-title="workItem.title"
data-testid="loading-types" :work-item-type="workItemType"
/> @error="error = $event"
<template v-else> />
<item-title
:initial-title="workItem.title"
data-testid="title"
@title-changed="updateWorkItem"
/>
</template>
</div>
</section> </section>
</template> </template>
...@@ -34987,9 +34987,6 @@ msgstr "" ...@@ -34987,9 +34987,6 @@ msgstr ""
msgid "Something went wrong while updating assignees" msgid "Something went wrong while updating assignees"
msgstr "" msgstr ""
msgid "Something went wrong while updating work item. Please try again"
msgstr ""
msgid "Something went wrong while updating your list settings" msgid "Something went wrong while updating your list settings"
msgstr "" msgstr ""
...@@ -42314,6 +42311,9 @@ msgstr "" ...@@ -42314,6 +42311,9 @@ msgstr ""
msgid "WorkItem|Something went wrong when fetching work item types. Please try again" msgid "WorkItem|Something went wrong when fetching work item types. Please try again"
msgstr "" msgstr ""
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
msgstr ""
msgid "WorkItem|Type" msgid "WorkItem|Type"
msgstr "" msgstr ""
......
...@@ -6,7 +6,6 @@ import { stubComponent } from 'helpers/stub_component'; ...@@ -6,7 +6,6 @@ import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { mockTracking } from 'helpers/tracking_helper'; import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createFlash from '~/flash';
import Description from '~/issues/show/components/description.vue'; import Description from '~/issues/show/components/description.vue';
import TaskList from '~/task_list'; import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
...@@ -320,15 +319,6 @@ describe('Description component', () => { ...@@ -320,15 +319,6 @@ describe('Description component', () => {
expect(findWorkItemDetailModal().props('visible')).toBe(false); expect(findWorkItemDetailModal().props('visible')).toBe(false);
}); });
it('shows error on error', async () => {
const message = 'I am error';
await findTaskLink().trigger('click');
findWorkItemDetailModal().vm.$emit('error', message);
expect(createFlash).toHaveBeenCalledWith({ message });
});
it('tracks when opened', async () => { it('tracks when opened', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
......
import { GlModal, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import WorkItemTitle from '~/work_items/components/item_title.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import { workItemQueryResponse } from '../mock_data'; import { workItemQueryResponse } from '../mock_data';
...@@ -13,10 +14,11 @@ describe('WorkItemDetailModal component', () => { ...@@ -13,10 +14,11 @@ describe('WorkItemDetailModal component', () => {
let wrapper; let wrapper;
Vue.use(VueApollo); Vue.use(VueApollo);
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const successHandler = jest.fn().mockResolvedValue({ data: workItemQueryResponse });
const findAlert = () => wrapper.findComponent(GlAlert);
const findModal = () => wrapper.findComponent(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const createComponent = ({ workItemId = '1', handler = successHandler } = {}) => { const createComponent = ({ workItemId = '1', handler = successHandler } = {}) => {
...@@ -41,10 +43,6 @@ describe('WorkItemDetailModal component', () => { ...@@ -41,10 +43,6 @@ describe('WorkItemDetailModal component', () => {
createComponent({ workItemId: null }); createComponent({ workItemId: null });
}); });
it('renders empty title when there is no `workItemId` prop', () => {
expect(findWorkItemTitle().exists()).toBe(true);
});
it('skips the work item query', () => { it('skips the work item query', () => {
expect(successHandler).not.toHaveBeenCalled(); expect(successHandler).not.toHaveBeenCalled();
}); });
...@@ -55,12 +53,10 @@ describe('WorkItemDetailModal component', () => { ...@@ -55,12 +53,10 @@ describe('WorkItemDetailModal component', () => {
createComponent(); createComponent();
}); });
it('renders loading spinner', () => { it('renders WorkItemTitle in loading state', () => {
expect(findLoadingIcon().exists()).toBe(true); createComponent();
});
it('does not render title', () => { expect(findWorkItemTitle().props('loading')).toBe(true);
expect(findWorkItemTitle().exists()).toBe(false);
}); });
}); });
...@@ -70,23 +66,26 @@ describe('WorkItemDetailModal component', () => { ...@@ -70,23 +66,26 @@ describe('WorkItemDetailModal component', () => {
return waitForPromises(); return waitForPromises();
}); });
it('does not render loading spinner', () => { it('does not render WorkItemTitle in loading state', () => {
expect(findLoadingIcon().exists()).toBe(false); expect(findWorkItemTitle().props('loading')).toBe(false);
});
it('renders title', () => {
expect(findWorkItemTitle().exists()).toBe(true);
}); });
}); });
it('emits an error if query has errored', async () => { it('shows an error message when the work item query was unsuccessful', async () => {
const errorHandler = jest.fn().mockRejectedValue('Oops'); const errorHandler = jest.fn().mockRejectedValue('Oops');
createComponent({ handler: errorHandler }); createComponent({ handler: errorHandler });
await waitForPromises();
expect(errorHandler).toHaveBeenCalled(); expect(errorHandler).toHaveBeenCalled();
expect(findAlert().text()).toBe(i18n.fetchError);
});
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
createComponent();
findWorkItemTitle().vm.$emit('error', i18n.updateError);
await waitForPromises(); await waitForPromises();
expect(wrapper.emitted('error')).toEqual([
['Something went wrong when fetching the work item. Please try again.'], expect(findAlert().text()).toBe(i18n.updateError);
]);
}); });
}); });
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import { i18n } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
describe('WorkItemTitle component', () => {
let wrapper;
Vue.use(VueApollo);
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findItemTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({ loading = false, mutationHandler = mutationSuccessHandler } = {}) => {
wrapper = shallowMount(WorkItemTitle, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
loading,
workItemId: workItemQueryResponse.workItem.id,
workItemTitle: workItemQueryResponse.workItem.title,
workItemType: workItemQueryResponse.workItem.workItemType.name,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('renders loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not render title', () => {
expect(findItemTitle().exists()).toBe(false);
});
});
describe('when loaded', () => {
beforeEach(() => {
createComponent({ loading: false });
});
it('does not render loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('renders title', () => {
expect(findItemTitle().props('initialTitle')).toBe(workItemQueryResponse.workItem.title);
});
});
describe('when updating the title', () => {
it('calls a mutation', () => {
const title = 'new title!';
createComponent();
findItemTitle().vm.$emit('title-changed', title);
expect(mutationSuccessHandler).toHaveBeenCalledWith({ input: { id: '1', title } });
});
it('does not call a mutation when the title has not changed', () => {
createComponent();
findItemTitle().vm.$emit('title-changed', workItemQueryResponse.workItem.title);
expect(mutationSuccessHandler).not.toHaveBeenCalled();
});
it('emits an error message when the mutation was unsuccessful', async () => {
createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
});
it('tracks editing the title', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
createComponent();
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_title', {
category: 'workItems:show',
label: 'item_title',
property: 'type_Task',
});
});
});
});
...@@ -6,6 +6,7 @@ export const workItemQueryResponse = { ...@@ -6,6 +6,7 @@ export const workItemQueryResponse = {
workItemType: { workItemType: {
__typename: 'WorkItemType', __typename: 'WorkItemType',
id: 'work-item-type-1', id: 'work-item-type-1',
name: 'Task',
}, },
widgets: { widgets: {
__typename: 'LocalWorkItemWidgetConnection', __typename: 'LocalWorkItemWidgetConnection',
...@@ -31,6 +32,7 @@ export const updateWorkItemMutationResponse = { ...@@ -31,6 +32,7 @@ export const updateWorkItemMutationResponse = {
workItemType: { workItemType: {
__typename: 'WorkItemType', __typename: 'WorkItemType',
id: 'work-item-type-1', id: 'work-item-type-1',
name: 'Task',
}, },
widgets: { widgets: {
__typename: 'LocalWorkItemWidgetConnection', __typename: 'LocalWorkItemWidgetConnection',
...@@ -73,6 +75,7 @@ export const createWorkItemMutationResponse = { ...@@ -73,6 +75,7 @@ export const createWorkItemMutationResponse = {
workItemType: { workItemType: {
__typename: 'WorkItemType', __typename: 'WorkItemType',
id: 'work-item-type-1', id: 'work-item-type-1',
name: 'Task',
}, },
}, },
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import ItemTitle from '~/work_items/components/item_title.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers'; import { i18n } from '~/work_items/constants';
import { workItemQueryResponse, updateWorkItemMutationResponse } from '../mock_data'; import { workItemQueryResponse } from '../mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
const WORK_ITEM_ID = '1'; const WORK_ITEM_ID = '1';
const WORK_ITEM_GID = `gid://gitlab/WorkItem/${WORK_ITEM_ID}`;
describe('Work items root component', () => { describe('Work items root component', () => {
const mockUpdatedTitle = 'Updated title';
let wrapper; let wrapper;
let fakeApollo;
const findTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
fakeApollo = createMockApollo(
[[updateWorkItemMutation, jest.fn().mockResolvedValue(updateWorkItemMutationResponse)]],
resolvers,
{
possibleTypes: {
LocalWorkItemWidget: ['LocalTitleWidget'],
},
},
);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
id: WORK_ITEM_GID,
},
data: queryResponse,
});
const successHandler = jest.fn().mockResolvedValue({ data: workItemQueryResponse });
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const createComponent = ({ handler = successHandler } = {}) => {
wrapper = shallowMount(WorkItemsRoot, { wrapper = shallowMount(WorkItemsRoot, {
apolloProvider: createMockApollo([[workItemQuery, handler]]),
propsData: { propsData: {
id: WORK_ITEM_ID, id: WORK_ITEM_ID,
}, },
apolloProvider: fakeApollo,
}); });
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
fakeApollo = null;
});
it('renders the title', () => {
createComponent();
expect(findTitle().exists()).toBe(true);
expect(findTitle().props('initialTitle')).toBe('Test');
}); });
it('updates the title when it is edited', async () => { describe('when loading', () => {
createComponent(); beforeEach(() => {
jest.spyOn(wrapper.vm.$apollo, 'mutate'); createComponent();
});
await findTitle().vm.$emit('title-changed', mockUpdatedTitle); it('renders WorkItemTitle in loading state', () => {
createComponent();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ expect(findWorkItemTitle().props('loading')).toBe(true);
mutation: updateWorkItemMutation,
variables: {
input: {
id: WORK_ITEM_GID,
title: mockUpdatedTitle,
},
},
}); });
}); });
describe('tracking', () => { describe('when loaded', () => {
let trackingSpy;
beforeEach(() => { beforeEach(() => {
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
createComponent(); createComponent();
return waitForPromises();
}); });
afterEach(() => { it('does not render WorkItemTitle in loading state', () => {
unmockTracking(); expect(findWorkItemTitle().props('loading')).toBe(false);
}); });
});
it('tracks item title updates', async () => { it('shows an error message when the work item query was unsuccessful', async () => {
await findTitle().vm.$emit('title-changed', mockUpdatedTitle); const errorHandler = jest.fn().mockRejectedValue('Oops');
createComponent({ handler: errorHandler });
await waitForPromises();
await waitForPromises(); expect(errorHandler).toHaveBeenCalled();
expect(findAlert().text()).toBe(i18n.fetchError);
});
expect(trackingSpy).toHaveBeenCalledTimes(1); it('shows an error message when WorkItemTitle emits an `error` event', async () => {
expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, { createComponent();
action: 'updated_title',
category: 'workItems:show', findWorkItemTitle().vm.$emit('error', i18n.updateError);
label: 'item_title', await waitForPromises();
property: '[type_work_item]',
}); expect(findAlert().text()).toBe(i18n.updateError);
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment