diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 10:00:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 10:00:54 +0000 |
commit | 3cccd102ba543e02725d247893729e5c73b38295 (patch) | |
tree | f36a04ec38517f5deaaacb5acc7d949688d1e187 /spec/frontend/work_items | |
parent | 205943281328046ef7b4528031b90fbda70c75ac (diff) | |
download | gitlab-ce-3cccd102ba543e02725d247893729e5c73b38295.tar.gz |
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'spec/frontend/work_items')
10 files changed, 492 insertions, 192 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index 0f6e7091c59..0d85df25b4f 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -4,10 +4,10 @@ import ItemTitle from '~/work_items/components/item_title.vue'; jest.mock('lodash/escape', () => jest.fn((fn) => fn)); -const createComponent = ({ initialTitle = 'Sample title', disabled = false } = {}) => +const createComponent = ({ title = 'Sample title', disabled = false } = {}) => shallowMount(ItemTitle, { propsData: { - initialTitle, + title, disabled, }, }); diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js new file mode 100644 index 00000000000..d0e9cfee353 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -0,0 +1,103 @@ +import { GlDropdownItem, GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import WorkItemActions from '~/work_items/components/work_item_actions.vue'; +import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql'; +import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data'; + +describe('WorkItemActions component', () => { + let wrapper; + let glModalDirective; + + Vue.use(VueApollo); + + const findModal = () => wrapper.findComponent(GlModal); + const findDeleteButton = () => wrapper.findComponent(GlDropdownItem); + + const createComponent = ({ + canUpdate = true, + deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse), + } = {}) => { + glModalDirective = jest.fn(); + wrapper = shallowMount(WorkItemActions, { + apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]), + propsData: { workItemId: '123', canUpdate }, + directives: { + glModal: { + bind(_, { value }) { + glModalDirective(value); + }, + }, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders modal', () => { + createComponent(); + + expect(findModal().exists()).toBe(true); + expect(findModal().props('visible')).toBe(false); + }); + + it('shows confirm modal when clicking Delete work item', () => { + createComponent(); + + findDeleteButton().vm.$emit('click'); + + expect(glModalDirective).toHaveBeenCalled(); + }); + + it('calls delete mutation when clicking OK button', () => { + const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse); + + createComponent({ + deleteWorkItemHandler, + }); + + findModal().vm.$emit('ok'); + + expect(deleteWorkItemHandler).toHaveBeenCalled(); + expect(wrapper.emitted('error')).toBeUndefined(); + }); + + it('emits event after delete success', async () => { + createComponent(); + + findModal().vm.$emit('ok'); + + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).not.toBeUndefined(); + expect(wrapper.emitted('error')).toBeUndefined(); + }); + + it('emits error event after delete failure', async () => { + createComponent({ + deleteWorkItemHandler: jest.fn().mockResolvedValue(deleteWorkItemFailureResponse), + }); + + findModal().vm.$emit('ok'); + + await waitForPromises(); + + expect(wrapper.emitted('error')[0]).toEqual([ + "The resource that you are attempting to access does not exist or you don't have permission to perform this action", + ]); + expect(wrapper.emitted('workItemDeleted')).toBeUndefined(); + }); + + it('does not render when canUpdate is false', () => { + createComponent({ + canUpdate: false, + }); + + expect(wrapper.html()).toBe(''); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js new file mode 100644 index 00000000000..9f35ccb853b --- /dev/null +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -0,0 +1,58 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; +import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; +import WorkItemActions from '~/work_items/components/work_item_actions.vue'; + +describe('WorkItemDetailModal component', () => { + let wrapper; + + Vue.use(VueApollo); + + const findModal = () => wrapper.findComponent(GlModal); + const findWorkItemActions = () => wrapper.findComponent(WorkItemActions); + const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); + + const createComponent = ({ visible = true, workItemId = '1', canUpdate = false } = {}) => { + wrapper = shallowMount(WorkItemDetailModal, { + propsData: { visible, workItemId, canUpdate }, + stubs: { + GlModal, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each([true, false])('when visible=%s', (visible) => { + it(`${visible ? 'renders' : 'does not render'} modal`, () => { + createComponent({ visible }); + + expect(findModal().props('visible')).toBe(visible); + }); + }); + + it('renders heading', () => { + createComponent(); + + expect(wrapper.find('h2').text()).toBe('Work Item'); + }); + + it('renders WorkItemDetail', () => { + createComponent(); + + expect(findWorkItemDetail().props()).toEqual({ workItemId: '1' }); + }); + + it('shows work item actions', () => { + createComponent({ + canUpdate: true, + }); + + expect(findWorkItemActions().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js deleted file mode 100644 index 305f43ad8ba..00000000000 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { GlModal } 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 WorkItemTitle from '~/work_items/components/item_title.vue'; -import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; -import { resolvers } from '~/work_items/graphql/resolvers'; - -describe('WorkItemDetailModal component', () => { - let wrapper; - - Vue.use(VueApollo); - - const findModal = () => wrapper.findComponent(GlModal); - const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); - - const createComponent = () => { - wrapper = shallowMount(WorkItemDetailModal, { - apolloProvider: createMockApollo([], resolvers), - propsData: { visible: true }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders modal', () => { - createComponent(); - - expect(findModal().props()).toMatchObject({ visible: true }); - }); - - it('renders work item title', () => { - createComponent(); - - expect(findWorkItemTitle().exists()).toBe(true); - }); -}); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js new file mode 100644 index 00000000000..9b1ef2d14e4 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -0,0 +1,117 @@ +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 } = {}) => { + const { id, title, workItemType } = workItemQueryResponse.data.workItem; + wrapper = shallowMount(WorkItemTitle, { + apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), + propsData: { + loading, + workItemId: id, + workItemTitle: title, + workItemType: 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('title')).toBe(workItemQueryResponse.data.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: workItemQueryResponse.data.workItem.id, + title, + }, + }); + }); + + it('does not call a mutation when the title has not changed', () => { + createComponent(); + + findItemTitle().vm.$emit('title-changed', workItemQueryResponse.data.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', + }); + }); + }); +}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 832795fc4ac..722e1708c15 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1,21 +1,14 @@ export const workItemQueryResponse = { - workItem: { - __typename: 'WorkItem', - id: '1', - title: 'Test', - workItemType: { - __typename: 'WorkItemType', - id: 'work-item-type-1', - }, - widgets: { - __typename: 'LocalWorkItemWidgetConnection', - nodes: [ - { - __typename: 'LocalTitleWidget', - type: 'TITLE', - contentText: 'Test', - }, - ], + data: { + workItem: { + __typename: 'WorkItem', + id: 'gid://gitlab/WorkItem/1', + title: 'Test', + workItemType: { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + }, }, }, }; @@ -23,25 +16,15 @@ export const workItemQueryResponse = { export const updateWorkItemMutationResponse = { data: { workItemUpdate: { - __typename: 'LocalUpdateWorkItemPayload', + __typename: 'WorkItemUpdatePayload', workItem: { - __typename: 'LocalWorkItem', - id: '1', + __typename: 'WorkItem', + id: 'gid://gitlab/WorkItem/1', title: 'Updated title', workItemType: { __typename: 'WorkItemType', - id: 'work-item-type-1', - }, - widgets: { - __typename: 'LocalWorkItemWidgetConnection', - nodes: [ - { - __typename: 'LocalTitleWidget', - type: 'TITLE', - enabled: true, - contentText: 'Updated title', - }, - ], + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', }, }, }, @@ -51,11 +34,11 @@ export const updateWorkItemMutationResponse = { export const projectWorkItemTypesQueryResponse = { data: { workspace: { - id: '1', + id: 'gid://gitlab/WorkItem/1', workItemTypes: { nodes: [ - { id: 'work-item-1', name: 'Issue' }, - { id: 'work-item-2', name: 'Incident' }, + { id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' }, + { id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' }, ], }, }, @@ -68,13 +51,53 @@ export const createWorkItemMutationResponse = { __typename: 'WorkItemCreatePayload', workItem: { __typename: 'WorkItem', - id: '1', + id: 'gid://gitlab/WorkItem/1', title: 'Updated title', workItemType: { __typename: 'WorkItemType', - id: 'work-item-type-1', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', }, }, }, }, }; + +export const createWorkItemFromTaskMutationResponse = { + data: { + workItemCreateFromTask: { + __typename: 'WorkItemCreateFromTaskPayload', + errors: [], + workItem: { + descriptionHtml: '<p>New description</p>', + id: 'gid://gitlab/WorkItem/13', + __typename: 'WorkItem', + }, + }, + }, +}; + +export const deleteWorkItemResponse = { + data: { workItemDelete: { errors: [], __typename: 'WorkItemDeletePayload' } }, +}; + +export const deleteWorkItemFailureResponse = { + data: { workItemDelete: null }, + errors: [ + { + message: + "The resource that you are attempting to access does not exist or you don't have permission to perform this action", + locations: [{ line: 2, column: 3 }], + path: ['workItemDelete'], + }, + ], +}; + +export const workItemTitleSubscriptionResponse = { + data: { + issuableTitleUpdated: { + id: 'gid://gitlab/WorkItem/1', + title: 'new title', + }, + }, +}; diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index 185b05c5191..fb1f1d56356 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -1,15 +1,19 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlAlert, GlFormSelect } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import ItemTitle from '~/work_items/components/item_title.vue'; -import { resolvers } from '~/work_items/graphql/resolvers'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; -import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data'; +import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql'; +import { + projectWorkItemTypesQueryResponse, + createWorkItemMutationResponse, + createWorkItemFromTaskMutationResponse, +} from '../mock_data'; jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] })); @@ -20,12 +24,15 @@ describe('Create work item component', () => { let fakeApollo; const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse); - const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse); + const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse); + const createWorkItemFromTaskSuccessHandler = jest + .fn() + .mockResolvedValue(createWorkItemFromTaskMutationResponse); + const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const findAlert = () => wrapper.findComponent(GlAlert); const findTitleInput = () => wrapper.findComponent(ItemTitle); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findSelect = () => wrapper.findComponent(GlFormSelect); const findCreateButton = () => wrapper.find('[data-testid="create-button"]'); const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]'); @@ -36,15 +43,13 @@ describe('Create work item component', () => { data = {}, props = {}, queryHandler = querySuccessHandler, - mutationHandler = mutationSuccessHandler, + mutationHandler = createWorkItemSuccessHandler, } = {}) => { - fakeApollo = createMockApollo( - [ - [projectWorkItemTypesQuery, queryHandler], - [createWorkItemMutation, mutationHandler], - ], - resolvers, - ); + fakeApollo = createMockApollo([ + [projectWorkItemTypesQuery, queryHandler], + [createWorkItemMutation, mutationHandler], + [createWorkItemFromTaskMutation, mutationHandler], + ]); wrapper = shallowMount(CreateWorkItem, { apolloProvider: fakeApollo, data() { @@ -123,6 +128,7 @@ describe('Create work item component', () => { props: { isModal: true, }, + mutationHandler: createWorkItemFromTaskSuccessHandler, }); }); @@ -133,14 +139,12 @@ describe('Create work item component', () => { }); it('emits `onCreate` on successful mutation', async () => { - const mockTitle = 'Test title'; findTitleInput().vm.$emit('title-input', 'Test title'); wrapper.find('form').trigger('submit'); await waitForPromises(); - const expected = { id: '1', title: mockTitle }; - expect(wrapper.emitted('onCreate')).toEqual([[expected]]); + expect(wrapper.emitted('onCreate')).toEqual([['<p>New description</p>']]); }); it('does not right margin for create button', () => { @@ -177,16 +181,14 @@ describe('Create work item component', () => { }); it('displays a list of work item types', () => { - expect(findDropdownItems()).toHaveLength(2); - expect(findDropdownItems().at(0).text()).toContain('Issue'); + expect(findSelect().attributes('options').split(',')).toHaveLength(3); }); it('selects a work item type on click', async () => { - expect(findDropdown().props('text')).toBe('Type'); - findDropdownItems().at(0).vm.$emit('click'); + const mockId = 'work-item-1'; + findSelect().vm.$emit('input', mockId); await nextTick(); - - expect(findDropdown().props('text')).toBe('Issue'); + expect(findSelect().attributes('value')).toBe(mockId); }); }); @@ -206,21 +208,36 @@ describe('Create work item component', () => { createComponent({ props: { initialTitle }, }); - expect(findTitleInput().props('initialTitle')).toBe(initialTitle); + expect(findTitleInput().props('title')).toBe(initialTitle); }); describe('when title input field has a text', () => { - beforeEach(() => { + beforeEach(async () => { const mockTitle = 'Test title'; createComponent(); + await waitForPromises(); findTitleInput().vm.$emit('title-input', mockTitle); }); - it('renders a non-disabled Create button', () => { + it('renders a disabled Create button', () => { + expect(findCreateButton().props('disabled')).toBe(true); + }); + + it('renders a non-disabled Create button when work item type is selected', async () => { + findSelect().vm.$emit('input', 'work-item-1'); + await nextTick(); expect(findCreateButton().props('disabled')).toBe(false); }); + }); + + it('shows an alert on mutation error', async () => { + createComponent({ mutationHandler: errorHandler }); + await waitForPromises(); + findTitleInput().vm.$emit('title-input', 'some title'); + findSelect().vm.$emit('input', 'work-item-1'); + wrapper.find('form').trigger('submit'); + await waitForPromises(); - // TODO: write a proper test here when we have a backend implementation - it.todo('shows an alert on mutation error'); + expect(findAlert().text()).toBe(CreateWorkItem.createErrorText); }); }); diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js new file mode 100644 index 00000000000..1eb6c0145e7 --- /dev/null +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -0,0 +1,99 @@ +import { GlAlert } 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 waitForPromises from 'helpers/wait_for_promises'; +import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; +import WorkItemTitle from '~/work_items/components/work_item_title.vue'; +import { i18n } from '~/work_items/constants'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; +import { workItemTitleSubscriptionResponse, workItemQueryResponse } from '../mock_data'; + +describe('WorkItemDetail component', () => { + let wrapper; + + Vue.use(VueApollo); + + const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + + const findAlert = () => wrapper.findComponent(GlAlert); + const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); + + const createComponent = ({ + workItemId = workItemQueryResponse.data.workItem.id, + handler = successHandler, + subscriptionHandler = initialSubscriptionHandler, + } = {}) => { + wrapper = shallowMount(WorkItemDetail, { + apolloProvider: createMockApollo([ + [workItemQuery, handler], + [workItemTitleSubscription, subscriptionHandler], + ]), + propsData: { workItemId }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when there is no `workItemId` prop', () => { + beforeEach(() => { + createComponent({ workItemId: null }); + }); + + it('skips the work item query', () => { + expect(successHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders WorkItemTitle in loading state', () => { + expect(findWorkItemTitle().props('loading')).toBe(true); + }); + }); + + describe('when loaded', () => { + beforeEach(() => { + createComponent(); + return waitForPromises(); + }); + + it('does not render WorkItemTitle in loading state', () => { + expect(findWorkItemTitle().props('loading')).toBe(false); + }); + }); + + it('shows an error message when the work item query was unsuccessful', async () => { + const errorHandler = jest.fn().mockRejectedValue('Oops'); + createComponent({ handler: errorHandler }); + await waitForPromises(); + + 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(); + + expect(findAlert().text()).toBe(i18n.updateError); + }); + + it('calls the subscription', () => { + createComponent(); + + expect(initialSubscriptionHandler).toHaveBeenCalledWith({ + issuableId: workItemQueryResponse.data.workItem.id, + }); + }); +}); diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js index 728495e0e23..2803724b9af 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -1,108 +1,31 @@ -import Vue from 'vue'; import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -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 updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; -import ItemTitle from '~/work_items/components/item_title.vue'; -import { resolvers } from '~/work_items/graphql/resolvers'; -import { workItemQueryResponse, updateWorkItemMutationResponse } from '../mock_data'; Vue.use(VueApollo); -const WORK_ITEM_ID = '1'; -const WORK_ITEM_GID = `gid://gitlab/WorkItem/${WORK_ITEM_ID}`; - describe('Work items root component', () => { - const mockUpdatedTitle = 'Updated title'; 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 findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); + const createComponent = () => { wrapper = shallowMount(WorkItemsRoot, { propsData: { - id: WORK_ITEM_ID, + id: '1', }, - apolloProvider: fakeApollo, }); }; afterEach(() => { wrapper.destroy(); - fakeApollo = null; }); - it('renders the title', () => { + it('renders WorkItemDetail', () => { createComponent(); - expect(findTitle().exists()).toBe(true); - expect(findTitle().props('initialTitle')).toBe('Test'); - }); - - it('updates the title when it is edited', async () => { - createComponent(); - jest.spyOn(wrapper.vm.$apollo, 'mutate'); - - await findTitle().vm.$emit('title-changed', mockUpdatedTitle); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateWorkItemMutation, - variables: { - input: { - id: WORK_ITEM_GID, - title: mockUpdatedTitle, - }, - }, - }); - }); - - describe('tracking', () => { - let trackingSpy; - - beforeEach(() => { - trackingSpy = mockTracking('_category_', undefined, jest.spyOn); - - createComponent(); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('tracks item title updates', async () => { - await findTitle().vm.$emit('title-changed', mockUpdatedTitle); - - await waitForPromises(); - - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, { - action: 'updated_title', - category: 'workItems:show', - label: 'item_title', - property: '[type_work_item]', - }); - }); + expect(findWorkItemDetail().props()).toEqual({ workItemId: 'gid://gitlab/WorkItem/1' }); }); }); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 8c9054920a8..7e68c5e4f0e 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -37,7 +37,7 @@ describe('Work items router', () => { it('renders work item on `/1` route', async () => { await createComponent('/1'); - expect(wrapper.find(WorkItemsRoot).exists()).toBe(true); + expect(wrapper.findComponent(WorkItemsRoot).exists()).toBe(true); }); it('renders create work item page on `/new` route', async () => { |