diff options
Diffstat (limited to 'spec/frontend/work_items')
11 files changed, 731 insertions, 42 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index 0d85df25b4f..2c3f6ef8634 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -15,7 +15,7 @@ const createComponent = ({ title = 'Sample title', disabled = false } = {}) => describe('ItemTitle', () => { let wrapper; const mockUpdatedTitle = 'Updated title'; - const findInputEl = () => wrapper.find('span#item-title'); + const findInputEl = () => wrapper.find('[aria-label="Title"]'); beforeEach(() => { wrapper = createComponent(); diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js new file mode 100644 index 00000000000..0552fe5050e --- /dev/null +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -0,0 +1,93 @@ +import { GlLink, GlTokenSelector } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; +import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql'; + +const mockAssignees = [ + { + __typename: 'UserCore', + id: 'gid://gitlab/User/1', + avatarUrl: '', + webUrl: '', + name: 'John Doe', + username: 'doe_I', + }, + { + __typename: 'UserCore', + id: 'gid://gitlab/User/2', + avatarUrl: '', + webUrl: '', + name: 'Marcus Rutherford', + username: 'ruthfull', + }, +]; + +const workItemId = 'gid://gitlab/WorkItem/1'; + +const mutate = jest.fn(); + +describe('WorkItemAssignees component', () => { + let wrapper; + + const findAssigneeLinks = () => wrapper.findAllComponents(GlLink); + const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); + + const findEmptyState = () => wrapper.findByTestId('empty-state'); + + const createComponent = ({ assignees = mockAssignees } = {}) => { + wrapper = mountExtended(WorkItemAssignees, { + propsData: { + assignees, + workItemId, + }, + mocks: { + $apollo: { + mutate, + }, + }, + attachTo: document.body, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should pass the correct data-user-id attribute', () => { + createComponent(); + + expect(findAssigneeLinks().at(0).attributes('data-user-id')).toBe('1'); + }); + + describe('when there are assignees', () => { + beforeEach(() => { + createComponent(); + }); + + it('should focus token selector on token removal', async () => { + findTokenSelector().vm.$emit('token-remove', mockAssignees[0].id); + await nextTick(); + + expect(findEmptyState().exists()).toBe(false); + expect(findTokenSelector().element.contains(document.activeElement)).toBe(true); + }); + + it('should call a mutation on clicking outside the token selector', async () => { + findTokenSelector().vm.$emit('input', [mockAssignees[0]]); + findTokenSelector().vm.$emit('token-remove'); + await nextTick(); + expect(mutate).not.toHaveBeenCalled(); + + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + await nextTick(); + + expect(mutate).toHaveBeenCalledWith({ + mutation: localUpdateWorkItemMutation, + variables: { + input: { id: workItemId, assigneeIds: [mockAssignees[0].id] }, + }, + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js new file mode 100644 index 00000000000..8017c46dea8 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -0,0 +1,222 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } 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 { updateDraft } from '~/lib/utils/autosave'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import WorkItemDescription from '~/work_items/components/work_item_description.vue'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import updateWorkItemWidgetsMutation from '~/work_items/graphql/update_work_item_widgets.mutation.graphql'; +import { + updateWorkItemWidgetsResponse, + workItemResponseFactory, + workItemQueryResponse, +} from '../mock_data'; + +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => { + return { + confirmAction: jest.fn(), + }; +}); +jest.mock('~/lib/utils/autosave'); + +const workItemId = workItemQueryResponse.data.workItem.id; + +describe('WorkItemDescription', () => { + let wrapper; + + Vue.use(VueApollo); + + const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse); + + const findEditButton = () => wrapper.find('[data-testid="edit-description"]'); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); + + const editDescription = (newText) => wrapper.find('textarea').setValue(newText); + + const clickCancel = () => wrapper.find('[data-testid="cancel"]').vm.$emit('click'); + const clickSave = () => wrapper.find('[data-testid="save-description"]').vm.$emit('click', {}); + + const createComponent = async ({ + mutationHandler = mutationSuccessHandler, + canUpdate = true, + isEditing = false, + } = {}) => { + const workItemResponse = workItemResponseFactory({ canUpdate }); + const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + + const { id } = workItemQueryResponse.data.workItem; + wrapper = shallowMount(WorkItemDescription, { + apolloProvider: createMockApollo([ + [workItemQuery, workItemResponseHandler], + [updateWorkItemWidgetsMutation, mutationHandler], + ]), + propsData: { + workItemId: id, + }, + provide: { + fullPath: '/group/project', + }, + stubs: { + MarkdownField, + }, + }); + + await waitForPromises(); + + if (isEditing) { + findEditButton().vm.$emit('click'); + + await nextTick(); + } + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Edit button', () => { + it('is not visible when canUpdate = false', async () => { + await createComponent({ + canUpdate: false, + }); + + expect(findEditButton().exists()).toBe(false); + }); + + it('toggles edit mode', async () => { + await createComponent({ + canUpdate: true, + }); + + findEditButton().vm.$emit('click'); + + await nextTick(); + + expect(findMarkdownField().exists()).toBe(true); + }); + }); + + describe('editing description', () => { + it('cancels when clicking cancel', async () => { + await createComponent({ + isEditing: true, + }); + + clickCancel(); + + await nextTick(); + + expect(confirmAction).not.toHaveBeenCalled(); + expect(findMarkdownField().exists()).toBe(false); + }); + + it('prompts for confirmation when clicking cancel after changes', async () => { + await createComponent({ + isEditing: true, + }); + + editDescription('updated desc'); + + clickCancel(); + + await nextTick(); + + expect(confirmAction).toHaveBeenCalled(); + }); + + it('calls update widgets mutation', async () => { + await createComponent({ + isEditing: true, + }); + + editDescription('updated desc'); + + clickSave(); + + await waitForPromises(); + + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: workItemId, + descriptionWidget: { + description: 'updated desc', + }, + }, + }); + }); + + it('tracks editing description', async () => { + await createComponent({ + isEditing: true, + markdownPreviewPath: '/preview', + }); + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + clickSave(); + + await waitForPromises(); + + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', { + category: TRACKING_CATEGORY_SHOW, + label: 'item_description', + property: 'type_Task', + }); + }); + + it('emits error when mutation returns error', async () => { + const error = 'eror'; + + await createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockResolvedValue({ + data: { + workItemUpdateWidgets: { + workItem: {}, + errors: [error], + }, + }, + }), + }); + + editDescription('updated desc'); + + clickSave(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[error]]); + }); + + it('emits error when mutation fails', async () => { + const error = 'eror'; + + await createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockRejectedValue(new Error(error)), + }); + + editDescription('updated desc'); + + clickSave(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[error]]); + }); + + it('autosaves description', async () => { + await createComponent({ + isEditing: true, + }); + + editDescription('updated desc'); + + expect(updateDraft).toHaveBeenCalled(); + }); + }); +}); 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 index aaabdbc82d9..d55ba318e46 100644 --- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -29,7 +29,7 @@ describe('WorkItemDetailModal component', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); - const createComponent = ({ workItemId = '1', error = false } = {}) => { + const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => { const apolloProvider = createMockApollo([ [ deleteWorkItemFromTaskMutation, @@ -46,7 +46,7 @@ describe('WorkItemDetailModal component', () => { wrapper = shallowMount(WorkItemDetailModal, { apolloProvider, - propsData: { workItemId }, + propsData: { workItemId, issueGid }, data() { return { error, @@ -67,6 +67,7 @@ describe('WorkItemDetailModal component', () => { expect(findWorkItemDetail().props()).toEqual({ workItemId: '1', + workItemParentId: '2', }); }); @@ -97,13 +98,6 @@ describe('WorkItemDetailModal component', () => { expect(wrapper.emitted('close')).toBeTruthy(); }); - it('emits `workItemUpdated` event on updating work item', () => { - createComponent(); - findWorkItemDetail().vm.$emit('workItemUpdated'); - - expect(wrapper.emitted('workItemUpdated')).toBeTruthy(); - }); - describe('delete work item', () => { it('emits workItemDeleted and closes modal', async () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js new file mode 100644 index 00000000000..774e9198992 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -0,0 +1,88 @@ +import Vue, { nextTick } from 'vue'; +import { GlBadge } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; +import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; +import { workItemHierarchyResponse, workItemHierarchyEmptyResponse } from '../../mock_data'; + +Vue.use(VueApollo); + +describe('WorkItemLinks', () => { + let wrapper; + + const createComponent = async ({ response = workItemHierarchyResponse } = {}) => { + wrapper = shallowMountExtended(WorkItemLinks, { + apolloProvider: createMockApollo([ + [getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)], + ]), + propsData: { issuableId: 1 }, + }); + + await waitForPromises(); + }; + + const findToggleButton = () => wrapper.findByTestId('toggle-links'); + const findLinksBody = () => wrapper.findByTestId('links-body'); + const findEmptyState = () => wrapper.findByTestId('links-empty'); + const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); + const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); + + beforeEach(async () => { + await createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('is expanded by default', () => { + expect(findToggleButton().props('icon')).toBe('chevron-lg-up'); + expect(findLinksBody().exists()).toBe(true); + }); + + it('expands on click toggle button', async () => { + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findToggleButton().props('icon')).toBe('chevron-lg-down'); + expect(findLinksBody().exists()).toBe(false); + }); + + describe('when no child links', () => { + beforeEach(async () => { + await createComponent({ response: workItemHierarchyEmptyResponse }); + }); + + it('displays empty state if there are no children', () => { + expect(findEmptyState().exists()).toBe(true); + }); + + describe('add link form', () => { + it('displays form on click add button and hides form on cancel', async () => { + expect(findEmptyState().exists()).toBe(true); + + findToggleAddFormButton().vm.$emit('click'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(true); + + findAddLinksForm().vm.$emit('cancel'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(false); + }); + }); + }); + + it('renders all hierarchy widget children', () => { + expect(findLinksBody().exists()).toBe(true); + + const children = wrapper.findAll('[data-testid="links-child"]'); + + expect(children).toHaveLength(4); + expect(children.at(0).findComponent(GlBadge).text()).toBe('Open'); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js index 9e48f56d9e9..b379d1fc846 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -12,6 +12,7 @@ import { STATE_CLOSED, STATE_EVENT_CLOSE, STATE_EVENT_REOPEN, + TRACKING_CATEGORY_SHOW, } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data'; @@ -81,15 +82,6 @@ describe('WorkItemState component', () => { }); }); - it('emits updated event', async () => { - createComponent(); - - findItemState().vm.$emit('changed', STATE_CLOSED); - await waitForPromises(); - - expect(wrapper.emitted('updated')).toEqual([[]]); - }); - it('emits an error message when the mutation was unsuccessful', async () => { createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') }); @@ -107,8 +99,8 @@ describe('WorkItemState component', () => { findItemState().vm.$emit('changed', STATE_CLOSED); await waitForPromises(); - expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_state', { - category: 'workItems:show', + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_state', { + category: TRACKING_CATEGORY_SHOW, label: 'item_state', property: 'type_Task', }); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index 19b56362ac0..a48449bb636 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -6,8 +6,9 @@ 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 { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data'; describe('WorkItemTitle component', () => { @@ -19,14 +20,18 @@ describe('WorkItemTitle component', () => { const findItemTitle = () => wrapper.findComponent(ItemTitle); - const createComponent = ({ mutationHandler = mutationSuccessHandler } = {}) => { + const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => { const { id, title, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemTitle, { - apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), + apolloProvider: createMockApollo([ + [updateWorkItemMutation, mutationHandler], + [updateWorkItemTaskMutation, mutationHandler], + ]), propsData: { workItemId: id, workItemTitle: title, workItemType: workItemType.name, + workItemParentId, }, }); }; @@ -57,13 +62,25 @@ describe('WorkItemTitle component', () => { }); }); - it('emits updated event', async () => { - createComponent(); + it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => { + const title = 'new title!'; + const workItemParentId = '1234'; - findItemTitle().vm.$emit('title-changed', 'new title'); - await waitForPromises(); + createComponent({ + workItemParentId, + }); - expect(wrapper.emitted('updated')).toEqual([[]]); + findItemTitle().vm.$emit('title-changed', title); + + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: workItemParentId, + taskData: { + id: workItemQueryResponse.data.workItem.id, + title, + }, + }, + }); }); it('does not call a mutation when the title has not changed', () => { @@ -91,8 +108,8 @@ describe('WorkItemTitle component', () => { findItemTitle().vm.$emit('title-changed', 'new title'); await waitForPromises(); - expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_title', { - category: 'workItems:show', + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_title', { + category: TRACKING_CATEGORY_SHOW, label: 'item_title', property: 'type_Task', }); diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js new file mode 100644 index 00000000000..80a1d032ad7 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_weight_spec.js @@ -0,0 +1,47 @@ +import { shallowMount } from '@vue/test-utils'; +import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; + +describe('WorkItemAssignees component', () => { + let wrapper; + + const createComponent = ({ weight, hasIssueWeightsFeature = true } = {}) => { + wrapper = shallowMount(WorkItemWeight, { + propsData: { + weight, + }, + provide: { + hasIssueWeightsFeature, + }, + }); + }; + + describe('weight licensed feature', () => { + describe.each` + description | hasIssueWeightsFeature | exists + ${'when available'} | ${true} | ${true} + ${'when not available'} | ${false} | ${false} + `('$description', ({ hasIssueWeightsFeature, exists }) => { + it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => { + createComponent({ hasIssueWeightsFeature }); + + expect(wrapper.find('div').exists()).toBe(exists); + }); + }); + }); + + describe('weight text', () => { + describe.each` + description | weight | text + ${'renders 1'} | ${1} | ${'1'} + ${'renders 0'} | ${0} | ${'0'} + ${'renders None'} | ${null} | ${'None'} + ${'renders None'} | ${undefined} | ${'None'} + `('when weight is $weight', ({ description, weight, text }) => { + it(description, () => { + createComponent({ weight }); + + expect(wrapper.text()).toContain(text); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index f3483550013..bf3f4e1364d 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -15,6 +15,15 @@ export const workItemQueryResponse = { deleteWorkItem: false, updateWorkItem: false, }, + widgets: [ + { + __typename: 'WorkItemWidgetDescription', + type: 'DESCRIPTION', + description: 'some **great** text', + descriptionHtml: + '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>', + }, + ], }, }, }; @@ -38,11 +47,53 @@ export const updateWorkItemMutationResponse = { deleteWorkItem: false, updateWorkItem: false, }, + widgets: [], }, }, }, }; +export const workItemResponseFactory = ({ canUpdate } = {}) => ({ + data: { + workItem: { + __typename: 'WorkItem', + id: 'gid://gitlab/WorkItem/1', + title: 'Updated title', + state: 'OPEN', + description: 'description', + workItemType: { + __typename: 'WorkItemType', + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + }, + userPermissions: { + deleteWorkItem: false, + updateWorkItem: canUpdate, + }, + widgets: [ + { + __typename: 'WorkItemWidgetDescription', + type: 'DESCRIPTION', + description: 'some **great** text', + descriptionHtml: + '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>', + }, + ], + }, + }, +}); + +export const updateWorkItemWidgetsResponse = { + data: { + workItemUpdateWidgets: { + workItem: { + id: 1234, + }, + errors: [], + }, + }, +}; + export const projectWorkItemTypesQueryResponse = { data: { workspace: { @@ -77,6 +128,7 @@ export const createWorkItemMutationResponse = { deleteWorkItem: false, updateWorkItem: false, }, + widgets: [], }, }, }, @@ -124,3 +176,102 @@ export const workItemTitleSubscriptionResponse = { }, }, }; + +export const workItemHierarchyEmptyResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/1', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + __typename: 'WorkItemType', + }, + title: 'New title', + widgets: [ + { + type: 'DESCRIPTION', + __typename: 'WorkItemWidgetDescription', + }, + { + type: 'HIERARCHY', + parent: null, + children: { + nodes: [], + __typename: 'WorkItemConnection', + }, + __typename: 'WorkItemWidgetHierarchy', + }, + ], + __typename: 'WorkItem', + }, + }, +}; + +export const workItemHierarchyResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/1', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + __typename: 'WorkItemType', + }, + title: 'New title', + widgets: [ + { + type: 'DESCRIPTION', + __typename: 'WorkItemWidgetDescription', + }, + { + type: 'HIERARCHY', + parent: null, + children: { + nodes: [ + { + id: 'gid://gitlab/WorkItem/2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'xyz', + state: 'OPEN', + __typename: 'WorkItem', + }, + { + id: 'gid://gitlab/WorkItem/3', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'abc', + state: 'CLOSED', + __typename: 'WorkItem', + }, + { + id: 'gid://gitlab/WorkItem/4', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'bar', + state: 'OPEN', + __typename: 'WorkItem', + }, + { + id: 'gid://gitlab/WorkItem/5', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'foobar', + state: 'OPEN', + __typename: 'WorkItem', + }, + ], + __typename: 'WorkItemConnection', + }, + __typename: 'WorkItemWidgetHierarchy', + }, + ], + __typename: 'WorkItem', + }, + }, +}; diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js index 9f87655175c..b9724034cb4 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -5,11 +5,15 @@ 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 WorkItemDescription from '~/work_items/components/work_item_description.vue'; import WorkItemState from '~/work_items/components/work_item_state.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; +import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; +import WorkItemWeight from '~/work_items/components/work_item_weight.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 { temporaryConfig } from '~/work_items/graphql/provider'; import { workItemTitleSubscriptionResponse, workItemQueryResponse } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -24,18 +28,34 @@ describe('WorkItemDetail component', () => { const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); const findWorkItemState = () => wrapper.findComponent(WorkItemState); + const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription); + const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); + const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight); const createComponent = ({ workItemId = workItemQueryResponse.data.workItem.id, handler = successHandler, subscriptionHandler = initialSubscriptionHandler, + workItemsMvc2Enabled = false, + includeWidgets = false, } = {}) => { wrapper = shallowMount(WorkItemDetail, { - apolloProvider: createMockApollo([ - [workItemQuery, handler], - [workItemTitleSubscription, subscriptionHandler], - ]), + apolloProvider: createMockApollo( + [ + [workItemQuery, handler], + [workItemTitleSubscription, subscriptionHandler], + ], + {}, + { + typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {}, + }, + ), propsData: { workItemId }, + provide: { + glFeatures: { + workItemsMvc2: workItemsMvc2Enabled, + }, + }, }); }; @@ -78,6 +98,22 @@ describe('WorkItemDetail component', () => { }); }); + describe('description', () => { + it('does not show description widget if loading description fails', () => { + createComponent(); + + expect(findWorkItemDescription().exists()).toBe(false); + }); + + it('shows description widget if description loads', async () => { + createComponent(); + + await waitForPromises(); + + expect(findWorkItemDescription().exists()).toBe(true); + }); + }); + it('shows an error message when the work item query was unsuccessful', async () => { const errorHandler = jest.fn().mockRejectedValue('Oops'); createComponent({ handler: errorHandler }); @@ -105,17 +141,64 @@ describe('WorkItemDetail component', () => { }); }); - it('emits workItemUpdated event when fields updated', async () => { - createComponent(); + describe('when work_items_mvc_2 feature flag is enabled', () => { + it('renders assignees component when assignees widget is returned from the API', async () => { + createComponent({ + workItemsMvc2Enabled: true, + includeWidgets: true, + }); + await waitForPromises(); - await waitForPromises(); + expect(findWorkItemAssignees().exists()).toBe(true); + }); - findWorkItemState().vm.$emit('updated'); + it('does not render assignees component when assignees widget is not returned from the API', async () => { + createComponent({ + workItemsMvc2Enabled: true, + includeWidgets: false, + }); + await waitForPromises(); - expect(wrapper.emitted('workItemUpdated')).toEqual([[]]); + expect(findWorkItemAssignees().exists()).toBe(false); + }); + }); - findWorkItemTitle().vm.$emit('updated'); + it('does not render assignees component when assignees feature flag is disabled', async () => { + createComponent(); + await waitForPromises(); - expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]); + expect(findWorkItemAssignees().exists()).toBe(false); + }); + + describe('weight widget', () => { + describe('when work_items_mvc_2 feature flag is enabled', () => { + describe.each` + description | includeWidgets | exists + ${'when widget is returned from API'} | ${true} | ${true} + ${'when widget is not returned from API'} | ${false} | ${false} + `('$description', ({ includeWidgets, exists }) => { + it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => { + createComponent({ includeWidgets, workItemsMvc2Enabled: true }); + await waitForPromises(); + + expect(findWorkItemWeight().exists()).toBe(exists); + }); + }); + }); + + describe('when work_items_mvc_2 feature flag is disabled', () => { + describe.each` + description | includeWidgets | exists + ${'when widget is returned from API'} | ${true} | ${false} + ${'when widget is not returned from API'} | ${false} | ${false} + `('$description', ({ includeWidgets, exists }) => { + it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => { + createComponent({ includeWidgets, workItemsMvc2Enabled: false }); + await waitForPromises(); + + expect(findWorkItemWeight().exists()).toBe(exists); + }); + }); + }); }); }); 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 85096392e84..3c5da94114e 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -11,6 +11,7 @@ import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graph import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), visitUrl: jest.fn(), })); @@ -52,6 +53,7 @@ describe('Work items root component', () => { expect(findWorkItemDetail().props()).toEqual({ workItemId: 'gid://gitlab/WorkItem/1', + workItemParentId: null, }); }); |