diff options
Diffstat (limited to 'spec/frontend/work_items/components/work_item_links')
4 files changed, 138 insertions, 32 deletions
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js index 47489d4796b..e693ccfb156 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js @@ -5,23 +5,22 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ItemMilestone from '~/issuable/components/issue_milestone.vue'; import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue'; -import { mockMilestone, mockAssignees, mockLabels } from '../../mock_data'; +import { workItemObjectiveMetadataWidgets } from '../../mock_data'; describe('WorkItemLinkChildMetadata', () => { + const { MILESTONE, ASSIGNEES, LABELS } = workItemObjectiveMetadataWidgets; + const mockMilestone = MILESTONE.milestone; + const mockAssignees = ASSIGNEES.assignees.nodes; + const mockLabels = LABELS.labels.nodes; let wrapper; - const createComponent = ({ - allowsScopedLabels = true, - milestone = mockMilestone, - assignees = mockAssignees, - labels = mockLabels, - } = {}) => { + const createComponent = ({ metadataWidgets = workItemObjectiveMetadataWidgets } = {}) => { wrapper = shallowMountExtended(WorkItemLinkChildMetadata, { propsData: { - allowsScopedLabels, - milestone, - assignees, - labels, + metadataWidgets, + }, + slots: { + default: `<div data-testid="default-slot">Foo</div>`, }, }); }; @@ -30,7 +29,11 @@ describe('WorkItemLinkChildMetadata', () => { createComponent(); }); - it('renders milestone link button', () => { + it('renders default slot contents', () => { + expect(wrapper.findByTestId('default-slot').text()).toBe('Foo'); + }); + + it('renders item milestone', () => { const milestoneLink = wrapper.findComponent(ItemMilestone); expect(milestoneLink.exists()).toBe(true); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index 73d498ad055..0470249d7ce 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -5,11 +5,12 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue'; + import { createAlert } from '~/flash'; import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql'; -import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue'; @@ -25,11 +26,9 @@ import { workItemObjectiveNoMetadata, confidentialWorkItemTask, closedWorkItemTask, - mockMilestone, - mockAssignees, - mockLabels, workItemHierarchyTreeResponse, workItemHierarchyTreeFailureResponse, + workItemObjectiveMetadataWidgets, } from '../../mock_data'; jest.mock('~/flash'); @@ -148,10 +147,7 @@ describe('WorkItemLinkChild', () => { const metadataEl = findMetadataComponent(); expect(metadataEl.exists()).toBe(true); expect(metadataEl.props()).toMatchObject({ - allowsScopedLabels: true, - milestone: mockMilestone, - assignees: mockAssignees, - labels: mockLabels, + metadataWidgets: workItemObjectiveMetadataWidgets, }); }); @@ -265,5 +261,14 @@ describe('WorkItemLinkChild', () => { message: 'Something went wrong while fetching children.', }); }); + + it('click event on child emits `click` event', async () => { + findExpandButton().vm.$emit('click'); + await waitForPromises(); + + findTreeChildren().vm.$emit('click', 'event'); + + expect(wrapper.emitted('click')).toEqual([['event']]); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index bbe460a55ba..5e1c46826cc 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -1,11 +1,18 @@ import Vue from 'vue'; -import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui'; +import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; +import { sprintf, s__ } from '~/locale'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; -import { FORM_TYPES } from '~/work_items/constants'; +import { + FORM_TYPES, + WORK_ITEM_TYPE_ENUM_TASK, + WORK_ITEM_TYPE_VALUE_ISSUE, + I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, + I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, +} from '~/work_items/constants'; import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; @@ -36,6 +43,8 @@ describe('WorkItemLinksForm', () => { workItemsMvcEnabled = false, parentIteration = null, formType = FORM_TYPES.create, + parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE, + childrenType = WORK_ITEM_TYPE_ENUM_TASK, } = {}) => { wrapper = shallowMountExtended(WorkItemLinksForm, { apolloProvider: createMockApollo([ @@ -48,6 +57,8 @@ describe('WorkItemLinksForm', () => { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential, parentIteration, + parentWorkItemType, + childrenType, formType, }, provide: { @@ -65,6 +76,7 @@ describe('WorkItemLinksForm', () => { const findForm = () => wrapper.findComponent(GlForm); const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); const findInput = () => wrapper.findComponent(GlFormInput); + const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findAddChildButton = () => wrapper.findByTestId('add-child-button'); afterEach(() => { @@ -90,6 +102,7 @@ describe('WorkItemLinksForm', () => { preventDefault: jest.fn(), }); await waitForPromises(); + expect(wrapper.vm.childWorkItemType).toEqual('gid://gitlab/WorkItems::Type/3'); expect(createMutationResolver).toHaveBeenCalledWith({ input: { title: 'Create task test', @@ -112,6 +125,7 @@ describe('WorkItemLinksForm', () => { preventDefault: jest.fn(), }); await waitForPromises(); + expect(wrapper.vm.childWorkItemType).toEqual('gid://gitlab/WorkItems::Type/3'); expect(createMutationResolver).toHaveBeenCalledWith({ input: { title: 'Create confidential task', @@ -124,9 +138,50 @@ describe('WorkItemLinksForm', () => { }, }); }); + + describe('confidentiality checkbox', () => { + it('renders confidentiality checkbox', () => { + const confidentialCheckbox = findConfidentialCheckbox(); + + expect(confidentialCheckbox.exists()).toBe(true); + expect(wrapper.findComponent(GlTooltip).exists()).toBe(false); + expect(confidentialCheckbox.text()).toBe( + sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, { + workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(), + }), + ); + }); + + it('renders confidentiality tooltip with checkbox checked and disabled when parent is confidential', () => { + createComponent({ parentConfidential: true }); + + const confidentialCheckbox = findConfidentialCheckbox(); + const confidentialTooltip = wrapper.findComponent(GlTooltip); + + expect(confidentialCheckbox.attributes('disabled')).toBe('true'); + expect(confidentialCheckbox.attributes('checked')).toBe('true'); + expect(confidentialTooltip.exists()).toBe(true); + expect(confidentialTooltip.text()).toBe( + sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, { + workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(), + parentWorkItemType: WORK_ITEM_TYPE_VALUE_ISSUE.toLocaleLowerCase(), + }), + ); + }); + }); }); describe('adding an existing work item', () => { + const selectAvailableWorkItemTokens = async () => { + findTokenSelector().vm.$emit( + 'input', + availableWorkItemsResponse.data.workspace.workItems.nodes, + ); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + + await waitForPromises(); + }; + beforeEach(async () => { await createComponent({ formType: FORM_TYPES.add }); }); @@ -136,6 +191,7 @@ describe('WorkItemLinksForm', () => { expect(findTokenSelector().exists()).toBe(true); expect(findAddChildButton().text()).toBe('Add task'); expect(findInput().exists()).toBe(false); + expect(findConfidentialCheckbox().exists()).toBe(false); }); it('searches for available work items as prop when typing in input', async () => { @@ -147,13 +203,7 @@ describe('WorkItemLinksForm', () => { }); it('selects and adds children', async () => { - findTokenSelector().vm.$emit( - 'input', - availableWorkItemsResponse.data.workspace.workItems.nodes, - ); - findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); - - await waitForPromises(); + await selectAvailableWorkItemTokens(); expect(findAddChildButton().text()).toBe('Add tasks'); findForm().vm.$emit('submit', { @@ -162,6 +212,31 @@ describe('WorkItemLinksForm', () => { await waitForPromises(); expect(updateMutationResolver).toHaveBeenCalled(); }); + + it('shows validation error when non-confidential child items are being added to confidential parent', async () => { + await createComponent({ formType: FORM_TYPES.add, parentConfidential: true }); + + await selectAvailableWorkItemTokens(); + + const validationEl = wrapper.findByTestId('work-items-invalid'); + expect(validationEl.exists()).toBe(true); + expect(validationEl.text().trim()).toBe( + sprintf( + s__( + 'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.', + ), + { + // Only non-confidential work items are shown in the error message + invalidWorkItemsList: availableWorkItemsResponse.data.workspace.workItems.nodes + .filter((wi) => !wi.confidential) + .map((wi) => wi.title) + .join(', '), + childWorkItemType: 'Task', + parentWorkItemType: 'Issue', + }, + ), + ); + }); }); describe('associate iteration with task', () => { diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 96211e12755..156f06a0d5e 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -34,6 +34,8 @@ describe('WorkItemTree', () => { const createComponent = ({ workItemType = 'Objective', + parentWorkItemType = 'Objective', + confidential = false, children = childrenWorkItems, apolloProvider = null, } = {}) => { @@ -55,7 +57,9 @@ describe('WorkItemTree', () => { apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]), propsData: { workItemType, + parentWorkItemType, workItemId: 'gid://gitlab/WorkItem/515', + confidential, children, projectPath: 'test/project', }, @@ -90,7 +94,11 @@ describe('WorkItemTree', () => { }); it('renders all hierarchy widget children', () => { - expect(findWorkItemLinkChildItems()).toHaveLength(4); + const workItemLinkChildren = findWorkItemLinkChildItems(); + expect(workItemLinkChildren).toHaveLength(4); + expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe( + childrenWorkItems[0].confidential, + ); }); it('does not display form by default', () => { @@ -110,8 +118,12 @@ describe('WorkItemTree', () => { await nextTick(); expect(findForm().exists()).toBe(true); - expect(findForm().props('formType')).toBe(formType); - expect(findForm().props('childrenType')).toBe(childType); + expect(findForm().props()).toMatchObject({ + formType, + childrenType: childType, + parentWorkItemType: 'Objective', + parentConfidential: false, + }); }, ); @@ -122,6 +134,17 @@ describe('WorkItemTree', () => { expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]); }); + it('emits `show-modal` on `click` event', () => { + const firstChild = findWorkItemLinkChildItems().at(0); + const event = { + childItem: 'gid://gitlab/WorkItem/2', + }; + + firstChild.vm.$emit('click', event); + + expect(wrapper.emitted('show-modal')).toEqual([[event, event.childItem]]); + }); + it.each` description | workItemType | prefetch ${'prefetches'} | ${'Issue'} | ${true} |