diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-14 06:11:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-14 06:11:17 +0300 |
commit | 886077c08875d595fc88a689f1ac841252813513 (patch) | |
tree | f47e7078289041816fb8a540bb12218a2cfccc27 /spec/frontend/work_items | |
parent | 71df3555b295779dec870c8ad59c30b6a47c837e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/work_items')
6 files changed, 350 insertions, 16 deletions
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js index 9a20e2ec98f..b86f9ff34ae 100644 --- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js +++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js @@ -10,7 +10,7 @@ import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue'; import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue'; -import { TASK_TYPE_NAME, WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants'; +import { WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants'; import { workItemTask, @@ -26,11 +26,9 @@ jest.mock('~/alert'); describe('WorkItemLinkChildContents', () => { Vue.use(VueApollo); - const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; let wrapper; const { LABELS } = workItemObjectiveMetadataWidgets; const mockLabels = LABELS.labels.nodes; - const mockFullPath = 'gitlab-org/gitlab-test'; const findStatusIconComponent = () => wrapper.findByTestId('item-status-icon').findComponent(GlIcon); @@ -43,19 +41,11 @@ describe('WorkItemLinkChildContents', () => { const findScopedLabel = () => findAllLabels().at(1); const findLinksMenuComponent = () => wrapper.findComponent(WorkItemLinksMenu); - const createComponent = ({ - canUpdate = true, - parentWorkItemId = WORK_ITEM_ID, - childItem = workItemTask, - workItemType = TASK_TYPE_NAME, - } = {}) => { + const createComponent = ({ canUpdate = true, childItem = workItemTask } = {}) => { wrapper = shallowMountExtended(WorkItemLinkChildContents, { propsData: { canUpdate, - parentWorkItemId, childItem, - workItemType, - fullPath: mockFullPath, childPath: '/gitlab-org/gitlab-test/-/work_items/4', }, }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 638a92dbd17..fec6d0673c6 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -20,6 +20,7 @@ import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_up import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; +import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; @@ -37,6 +38,7 @@ import { workItemByIidResponseFactory, objectiveType, mockWorkItemCommentNote, + mockBlockingLinkedItem, } from '../mock_data'; jest.mock('~/lib/utils/common_utils'); @@ -76,6 +78,7 @@ describe('WorkItemDetail component', () => { const findCloseButton = () => wrapper.findByTestId('work-item-close'); const findWorkItemType = () => wrapper.findByTestId('work-item-type'); const findHierarchyTree = () => wrapper.findComponent(WorkItemTree); + const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships); const findNotesWidget = () => wrapper.findComponent(WorkItemNotes); const findModal = () => wrapper.findComponent(WorkItemDetailModal); const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); @@ -96,6 +99,7 @@ describe('WorkItemDetail component', () => { confidentialityMock = [updateWorkItemMutation, jest.fn()], error = undefined, workItemsMvc2Enabled = false, + linkedWorkItemsEnabled = false, } = {}) => { const handlers = [ [workItemByIidQuery, handler], @@ -119,6 +123,7 @@ describe('WorkItemDetail component', () => { provide: { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, + linkedWorkItems: linkedWorkItemsEnabled, }, hasIssueWeightsFeature: true, hasIterationsFeature: true, @@ -581,6 +586,82 @@ describe('WorkItemDetail component', () => { }); }); + describe('relationship widget', () => { + it('does not render linked items by default', async () => { + createComponent(); + await waitForPromises(); + + expect(findWorkItemRelationships().exists()).toBe(false); + }); + + describe('work item has children', () => { + const mockWorkItemLinkedItem = workItemByIidResponseFactory({ + linkedItems: mockBlockingLinkedItem, + }); + const handler = jest.fn().mockResolvedValue(mockWorkItemLinkedItem); + + it('renders relationship widget when work item has linked items', async () => { + createComponent({ handler, linkedWorkItemsEnabled: true }); + await waitForPromises(); + + expect(findWorkItemRelationships().exists()).toBe(true); + }); + + it('opens the modal with the linked item when `showModal` is emitted', async () => { + createComponent({ + handler, + linkedWorkItemsEnabled: true, + workItemsMvc2Enabled: true, + }); + await waitForPromises(); + + const event = { + preventDefault: jest.fn(), + }; + + findWorkItemRelationships().vm.$emit('showModal', { + event, + modalWorkItem: { id: 'childWorkItemId' }, + }); + await waitForPromises(); + + expect(findModal().props().workItemId).toBe('childWorkItemId'); + expect(showModalHandler).toHaveBeenCalled(); + }); + + describe('linked work item is rendered in a modal and has linked items', () => { + beforeEach(async () => { + createComponent({ + isModal: true, + handler, + workItemsMvc2Enabled: true, + linkedWorkItemsEnabled: true, + }); + + await waitForPromises(); + }); + + it('does not render a new modal', () => { + expect(findModal().exists()).toBe(false); + }); + + it('emits `update-modal` when `show-modal` is emitted', async () => { + const event = { + preventDefault: jest.fn(), + }; + + findWorkItemRelationships().vm.$emit('showModal', { + event, + modalWorkItem: { id: 'childWorkItemId' }, + }); + await waitForPromises(); + + expect(wrapper.emitted('update-modal')).toBeDefined(); + }); + }); + }); + }); + describe('notes widget', () => { it('renders notes by default', async () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap new file mode 100644 index 00000000000..9105e4de5e0 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WorkItemRelationshipList renders linked item list 1`] = ` +<div> + <h4 + class="gl-font-sm gl-font-weight-semibold gl-mb-2 gl-mt-3 gl-mx-2 gl-text-gray-700" + data-testid="work-items-list-heading" + > + Blocking + </h4> + <div + class="work-items-list-body" + > + <ul + class="content-list work-items-list" + > + <li + class="gl-border-b-0! gl-pb-0! gl-pt-0!" + > + <work-item-link-child-contents-stub + canupdate="true" + childitem="[object Object]" + childpath="/test-project-path/-/work_items/83" + /> + </li> + </ul> + </div> +</div> +`; diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js new file mode 100644 index 00000000000..759ab7e14da --- /dev/null +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js @@ -0,0 +1,41 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue'; +import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue'; + +import { mockBlockingLinkedItem } from '../../mock_data'; + +describe('WorkItemRelationshipList', () => { + let wrapper; + const mockLinkedItems = mockBlockingLinkedItem.linkedItems.nodes; + + const createComponent = ({ linkedItems = [], heading = 'Blocking', canUpdate = true } = {}) => { + wrapper = shallowMountExtended(WorkItemRelationshipList, { + propsData: { + linkedItems, + heading, + canUpdate, + workItemFullPath: 'test-project-path', + }, + }); + }; + + const findHeading = () => wrapper.findByTestId('work-items-list-heading'); + const findWorkItemLinkChildContents = () => wrapper.findComponent(WorkItemLinkChildContents); + + beforeEach(() => { + createComponent({ linkedItems: mockLinkedItems }); + }); + + it('renders linked item list', () => { + expect(findHeading().text()).toBe('Blocking'); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('renders work item link child contents with correct props', () => { + expect(findWorkItemLinkChildContents().props()).toMatchObject({ + childItem: mockLinkedItems[0].workItem, + canUpdate: true, + childPath: '/test-project-path/-/work_items/83', + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js index 18d13aa251e..c9a2499b127 100644 --- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js @@ -1,25 +1,93 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon } from '@gitlab/ui'; + +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; + +import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue'; +import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; + +import { + workItemByIidResponseFactory, + mockLinkedItems, + mockBlockingLinkedItem, +} from '../../mock_data'; describe('WorkItemRelationships', () => { + Vue.use(VueApollo); + let wrapper; + const emptyLinkedWorkItemsQueryHandler = jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory()); + const linkedWorkItemsQueryHandler = jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })); + const blockingLinkedWorkItemQueryHandler = jest + .fn() + .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem })); + + const createComponent = async ({ + workItemQueryHandler = emptyLinkedWorkItemsQueryHandler, + } = {}) => { + const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]); - const createComponent = async () => { wrapper = shallowMountExtended(WorkItemRelationships, { + apolloProvider: mockApollo, propsData: { - workItem: {}, workItemIid: '1', - workItemFullpath: 'gitlab/path', + workItemFullPath: 'test-project-path', }, }); await waitForPromises(); }; - it('renders the component', () => { + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper); + const findEmptyRelatedMessageContainer = () => wrapper.findByTestId('links-empty'); + const findLinkedItemsCountContainer = () => wrapper.findByTestId('linked-items-count'); + const findAllWorkItemRelationshipListComponents = () => + wrapper.findAllComponents(WorkItemRelationshipList); + + it('shows loading icon when query is not processed', () => { createComponent(); + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('renders the component with empty message when there are no items', async () => { + await createComponent(); + expect(wrapper.find('.work-item-relationships').exists()).toBe(true); + expect(findEmptyRelatedMessageContainer().exists()).toBe(true); + }); + + it('renders blocking linked item lists', async () => { + await createComponent({ workItemQueryHandler: blockingLinkedWorkItemQueryHandler }); + + expect(findAllWorkItemRelationshipListComponents().length).toBe(1); + expect(findLinkedItemsCountContainer().text()).toBe('1'); + }); + + it('renders blocking, blocked by and related to linked item lists with proper count', async () => { + await createComponent({ workItemQueryHandler: linkedWorkItemsQueryHandler }); + + // renders all 3 lists: blocking, blocked by and related to + expect(findAllWorkItemRelationshipListComponents().length).toBe(3); + expect(findLinkedItemsCountContainer().text()).toBe('3'); + }); + + it('shows an alert when list loading fails', async () => { + const errorMessage = 'Some error'; + await createComponent({ + workItemQueryHandler: jest.fn().mockRejectedValue(new Error(errorMessage)), + }); + + expect(findWidgetWrapper().props('error')).toBe(errorMessage); }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 3c430996a59..ba244b19eb5 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1,3 +1,5 @@ +import { WIDGET_TYPE_LINKED_ITEMS } from '~/work_items/constants'; + export const mockAssignees = [ { __typename: 'UserCore', @@ -451,6 +453,126 @@ export const objectiveType = { iconName: 'issue-type-objective', }; +export const mockEmptyLinkedItems = { + type: WIDGET_TYPE_LINKED_ITEMS, + blocked: false, + blockedByCount: 0, + blockingCount: 0, + linkedItems: { + nodes: [], + __typename: 'LinkedWorkItemTypeConnection', + }, + __typename: 'WorkItemWidgetLinkedItems', +}; + +export const mockBlockingLinkedItem = { + type: WIDGET_TYPE_LINKED_ITEMS, + linkedItems: { + nodes: [ + { + linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/8', + linkType: 'blocks', + workItem: { + id: 'gid://gitlab/WorkItem/675', + iid: '83', + confidential: true, + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', + __typename: 'WorkItemType', + }, + title: 'Task 1201', + state: 'OPEN', + createdAt: '2023-03-28T10:50:16Z', + closedAt: null, + widgets: [], + __typename: 'WorkItem', + }, + __typename: 'LinkedWorkItemType', + }, + ], + __typename: 'LinkedWorkItemTypeConnection', + }, + __typename: 'WorkItemWidgetLinkedItems', +}; + +export const mockLinkedItems = { + type: WIDGET_TYPE_LINKED_ITEMS, + linkedItems: { + nodes: [ + { + linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/8', + linkType: 'relates_to', + workItem: { + id: 'gid://gitlab/WorkItem/675', + iid: '83', + confidential: true, + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + name: 'Task', + iconName: 'issue-type-task', + __typename: 'WorkItemType', + }, + title: 'Task 1201', + state: 'OPEN', + createdAt: '2023-03-28T10:50:16Z', + closedAt: null, + widgets: [], + __typename: 'WorkItem', + }, + __typename: 'LinkedWorkItemType', + }, + { + linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/9', + linkType: 'is_blocked_by', + workItem: { + id: 'gid://gitlab/WorkItem/646', + iid: '55', + confidential: true, + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + title: 'Multilevel Objective 1', + state: 'OPEN', + createdAt: '2023-03-28T10:50:16Z', + closedAt: null, + widgets: [], + __typename: 'WorkItem', + }, + __typename: 'LinkedWorkItemType', + }, + { + linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/10', + linkType: 'blocks', + workItem: { + id: 'gid://gitlab/WorkItem/647', + iid: '56', + confidential: true, + workItemType: { + id: 'gid://gitlab/WorkItems::Type/6', + name: 'Objective', + iconName: 'issue-type-objective', + __typename: 'WorkItemType', + }, + title: 'Multilevel Objective 2', + state: 'OPEN', + createdAt: '2023-03-28T10:50:16Z', + closedAt: null, + widgets: [], + __typename: 'WorkItem', + }, + __typename: 'LinkedWorkItemType', + }, + ], + __typename: 'LinkedWorkItemTypeConnection', + }, + __typename: 'WorkItemWidgetLinkedItems', +}; + export const workItemResponseFactory = ({ iid = '1', canUpdate = false, @@ -473,6 +595,7 @@ export const workItemResponseFactory = ({ confidential = false, canInviteMembers = false, labelsWidgetPresent = true, + linkedItemsWidgetPresent = true, labels = mockLabels, allowsScopedLabels = false, lastEditedAt = null, @@ -485,6 +608,7 @@ export const workItemResponseFactory = ({ updatedAt = '2022-08-08T12:32:54Z', awardEmoji = mockAwardsWidget, state = 'OPEN', + linkedItems = mockEmptyLinkedItems, } = {}) => ({ data: { workItem: { @@ -683,6 +807,7 @@ export const workItemResponseFactory = ({ awardEmoji, } : { type: 'MOCK TYPE' }, + linkedItemsWidgetPresent ? linkedItems : { type: 'MOCK TYPE' }, ], }, }, |