diff options
Diffstat (limited to 'spec/frontend/work_items')
15 files changed, 1075 insertions, 77 deletions
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap new file mode 100644 index 00000000000..52838dcd0bc --- /dev/null +++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Work Item Note Body should have the wrapper to show the note body 1`] = ` +"<div data-testid=\\"work-item-note-body\\" class=\\"note-text md\\"> + <p dir=\\"auto\\" data-sourcepos=\\"1:1-1:76\\"> + <gl-emoji data-unicode-version=\\"6.0\\" data-name=\\"wave\\" title=\\"waving hand sign\\">👋</gl-emoji> Hi <a title=\\"Sherie Nitzsche\\" class=\\"gfm gfm-project_member js-user-link\\" data-placement=\\"top\\" data-container=\\"body\\" data-user=\\"3\\" data-reference-type=\\"user\\" href=\\"/fredda.brekke\\">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji data-unicode-version=\\"6.0\\" data-name=\\"pray\\" title=\\"person with folded hands\\">🙏</gl-emoji> + </p> +</div>" +`; diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js new file mode 100644 index 00000000000..eb4bcbf942b --- /dev/null +++ b/spec/frontend/work_items/components/notes/activity_filter_spec.js @@ -0,0 +1,74 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { ASC, DESC } from '~/notes/constants'; + +import { mockTracking } from 'helpers/tracking_helper'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; + +describe('Activity Filter', () => { + let wrapper; + + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first'); + + const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => { + wrapper = shallowMountExtended(ActivityFilter, { + propsData: { + sortOrder, + loading, + workItemType, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + describe('default', () => { + it('has a dropdown with 2 options', () => { + expect(findDropdown().exists()).toBe(true); + expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length); + }); + + it('has local storage sync with the correct props', () => { + expect(findLocalStorageSync().props('asString')).toBe(true); + }); + + it('emits `updateSavedSortOrder` event when update is emitted', async () => { + findLocalStorageSync().vm.$emit('input', ASC); + + await nextTick(); + expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1); + expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]); + }); + }); + + describe('when asc', () => { + describe('when the dropdown is clicked', () => { + it('calls the right actions', async () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + findNewestFirstItem().vm.$emit('click'); + await nextTick(); + + expect(wrapper.emitted('changeSortOrder')).toHaveLength(1); + expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]); + + expect(trackingSpy).toHaveBeenCalledWith( + TRACKING_CATEGORY_SHOW, + 'notes_sort_order_changed', + { + category: TRACKING_CATEGORY_SHOW, + label: 'item_track_notes_sorting', + property: 'type_Task', + }, + ); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_note_body_spec.js b/spec/frontend/work_items/components/notes/work_item_note_body_spec.js new file mode 100644 index 00000000000..4fcbcfcaf30 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_note_body_spec.js @@ -0,0 +1,32 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemNoteBody from '~/work_items/components/notes/work_item_note_body.vue'; +import NoteEditedText from '~/notes/components/note_edited_text.vue'; +import { mockWorkItemCommentNote } from 'jest/work_items/mock_data'; + +describe('Work Item Note Body', () => { + let wrapper; + + const findNoteBody = () => wrapper.findByTestId('work-item-note-body'); + const findNoteEditedText = () => wrapper.findComponent(NoteEditedText); + + const createComponent = ({ note = mockWorkItemCommentNote } = {}) => { + wrapper = shallowMountExtended(WorkItemNoteBody, { + propsData: { + note, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('should have the wrapper to show the note body', () => { + expect(findNoteBody().exists()).toBe(true); + expect(findNoteBody().html()).toMatchSnapshot(); + }); + + it('should not show the edited text when the value is not present', () => { + expect(findNoteEditedText().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js new file mode 100644 index 00000000000..7257d5c8023 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -0,0 +1,53 @@ +import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import WorkItemNote from '~/work_items/components/notes/work_item_note.vue'; +import NoteBody from '~/work_items/components/notes/work_item_note_body.vue'; +import NoteHeader from '~/notes/components/note_header.vue'; +import { mockWorkItemCommentNote } from 'jest/work_items/mock_data'; + +describe('Work Item Note', () => { + let wrapper; + + const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem); + const findNoteHeader = () => wrapper.findComponent(NoteHeader); + const findNoteBody = () => wrapper.findComponent(NoteBody); + const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); + const findAvatar = () => wrapper.findComponent(GlAvatar); + + const createComponent = ({ note = mockWorkItemCommentNote } = {}) => { + wrapper = shallowMount(WorkItemNote, { + propsData: { + note, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('Should be wrapped inside the timeline entry item', () => { + expect(findTimelineEntryItem().exists()).toBe(true); + }); + + it('should have the author avatar of the work item note', () => { + expect(findAvatarLink().exists()).toBe(true); + expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl); + + expect(findAvatar().exists()).toBe(true); + expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl); + expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username); + }); + + it('has note header', () => { + expect(findNoteHeader().exists()).toBe(true); + expect(findNoteHeader().props('author')).toEqual(mockWorkItemCommentNote.author); + expect(findNoteHeader().props('createdAt')).toBe(mockWorkItemCommentNote.createdAt); + }); + + it('has note body', () => { + expect(findNoteBody().exists()).toBe(true); + expect(findNoteBody().props('note')).toEqual(mockWorkItemCommentNote); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/work_item_comment_form_spec.js new file mode 100644 index 00000000000..07c00119398 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_comment_form_spec.js @@ -0,0 +1,205 @@ +import { GlButton } 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 { updateDraft } from '~/lib/utils/autosave'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; +import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue'; +import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue'; +import createNoteMutation from '~/work_items/graphql/create_work_item_note.mutation.graphql'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; +import { + workItemResponseFactory, + workItemQueryResponse, + projectWorkItemResponse, + createWorkItemNoteResponse, +} from '../mock_data'; + +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); +jest.mock('~/lib/utils/autosave'); + +const workItemId = workItemQueryResponse.data.workItem.id; + +describe('WorkItemCommentForm', () => { + let wrapper; + + Vue.use(VueApollo); + + const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse); + const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse); + let workItemResponseHandler; + + const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); + + const setText = (newText) => { + return findMarkdownEditor().vm.$emit('input', newText); + }; + + const clickSave = () => + wrapper + .findAllComponents(GlButton) + .filter((button) => button.text().startsWith('Comment')) + .at(0) + .vm.$emit('click', {}); + + const createComponent = async ({ + mutationHandler = mutationSuccessHandler, + canUpdate = true, + workItemResponse = workItemResponseFactory({ canUpdate }), + queryVariables = { id: workItemId }, + fetchByIid = false, + signedIn = true, + isEditing = true, + } = {}) => { + workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + + if (signedIn) { + window.gon.current_user_id = '1'; + window.gon.current_user_avatar_url = 'avatar.png'; + } + + const { id } = workItemQueryResponse.data.workItem; + wrapper = shallowMount(WorkItemCommentForm, { + apolloProvider: createMockApollo([ + [workItemQuery, workItemResponseHandler], + [createNoteMutation, mutationHandler], + [workItemByIidQuery, workItemByIidResponseHandler], + ]), + propsData: { + workItemId: id, + fullPath: 'test-project-path', + queryVariables, + fetchByIid, + }, + stubs: { + MarkdownField, + WorkItemCommentLocked, + }, + }); + + await waitForPromises(); + + if (isEditing) { + wrapper.findComponent(GlButton).vm.$emit('click'); + } + }; + + describe('adding a comment', () => { + it('calls update widgets mutation', async () => { + const noteText = 'updated desc'; + + await createComponent({ + isEditing: true, + signedIn: true, + }); + + setText(noteText); + + clickSave(); + + await waitForPromises(); + + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + noteableId: workItemId, + body: noteText, + }, + }); + }); + + it('tracks adding comment', async () => { + await createComponent(); + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + setText('test'); + + clickSave(); + + await waitForPromises(); + + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'add_work_item_comment', { + category: TRACKING_CATEGORY_SHOW, + label: 'item_comment', + property: 'type_Task', + }); + }); + + it('emits error when mutation returns error', async () => { + const error = 'eror'; + + await createComponent({ + isEditing: true, + mutationHandler: jest.fn().mockResolvedValue({ + data: { + createNote: { + note: null, + errors: [error], + }, + }, + }), + }); + + setText('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)), + }); + + setText('updated desc'); + + clickSave(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[error]]); + }); + + it('autosaves', async () => { + await createComponent({ + isEditing: true, + }); + + setText('updated'); + + expect(updateDraft).toHaveBeenCalled(); + }); + }); + + it('calls the global ID work item query when `fetchByIid` prop is false', async () => { + createComponent({ fetchByIid: false }); + await waitForPromises(); + + expect(workItemResponseHandler).toHaveBeenCalled(); + expect(workItemByIidResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the IID work item query when when `fetchByIid` prop is true', async () => { + await createComponent({ fetchByIid: true, isEditing: false }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + expect(workItemByIidResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the handlers when missing the needed queryVariables', async () => { + await createComponent({ queryVariables: {}, fetchByIid: false, isEditing: false }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_comment_locked_spec.js b/spec/frontend/work_items/components/work_item_comment_locked_spec.js new file mode 100644 index 00000000000..58491c4b09c --- /dev/null +++ b/spec/frontend/work_items/components/work_item_comment_locked_spec.js @@ -0,0 +1,41 @@ +import { GlLink, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue'; + +const createComponent = ({ workItemType = 'Task', isProjectArchived = false } = {}) => + shallowMount(WorkItemCommentLocked, { + propsData: { + workItemType, + isProjectArchived, + }, + }); + +describe('WorkItemCommentLocked', () => { + let wrapper; + const findLockedIcon = () => wrapper.findComponent(GlIcon); + const findLearnMoreLink = () => wrapper.findComponent(GlLink); + + it('renders the locked icon', () => { + wrapper = createComponent(); + expect(findLockedIcon().props('name')).toBe('lock'); + }); + + it('has the learn more link', () => { + wrapper = createComponent(); + expect(findLearnMoreLink().attributes('href')).toBe( + WorkItemCommentLocked.constantOptions.lockedIssueDocsPath, + ); + }); + + describe('when the project is archived', () => { + beforeEach(() => { + wrapper = createComponent({ isProjectArchived: true }); + }); + + it('learn more link is directed to archived project docs path', () => { + expect(findLearnMoreLink().attributes('href')).toBe( + WorkItemCommentLocked.constantOptions.archivedProjectDocsPath, + ); + }); + }); +}); 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 686641800b3..8976cd6e22b 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 @@ -4,10 +4,11 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; -import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; +import { stubComponent } from 'helpers/stub_component'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql'; import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql'; +import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import { deleteWorkItemFromTaskMutationErrorResponse, deleteWorkItemFromTaskMutationResponse, @@ -69,8 +70,14 @@ describe('WorkItemDetailModal component', () => { error, }; }, + provide: { + fullPath: 'group/project', + }, stubs: { GlModal, + WorkItemDetail: stubComponent(WorkItemDetail, { + apollo: {}, + }), }, }); }; @@ -126,6 +133,15 @@ describe('WorkItemDetailModal component', () => { expect(closeSpy).toHaveBeenCalled(); }); + it('updates the work item when WorkItemDetail emits `update-modal` event', async () => { + createComponent(); + + findWorkItemDetail().vm.$emit('update-modal', null, 'updatedId'); + await waitForPromises(); + + expect(findWorkItemDetail().props().workItemId).toEqual('updatedId'); + }); + describe('delete work item', () => { describe('when there is task data', () => { it('emits workItemDeleted and closes modal', async () => { 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 bbab45c7055..a50a48de921 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -12,6 +12,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import setWindowLocation from 'helpers/set_window_location_helper'; +import { stubComponent } from 'helpers/stub_component'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; @@ -22,6 +23,8 @@ import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue'; +import WorkItemNotes from '~/work_items/components/work_item_notes.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 workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; @@ -63,6 +66,7 @@ describe('WorkItemDetail component', () => { const assigneesSubscriptionHandler = jest .fn() .mockResolvedValue(workItemAssigneesSubscriptionResponse); + const showModalHandler = jest.fn(); const findAlert = () => wrapper.findComponent(GlAlert); const findEmptyState = () => wrapper.findComponent(GlEmptyState); @@ -81,6 +85,8 @@ describe('WorkItemDetail component', () => { const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]'); const findHierarchyTree = () => wrapper.findComponent(WorkItemTree); + const findNotesWidget = () => wrapper.findComponent(WorkItemNotes); + const findModal = () => wrapper.findComponent(WorkItemDetailModal); const createComponent = ({ isModal = false, @@ -129,6 +135,12 @@ describe('WorkItemDetail component', () => { stubs: { WorkItemWeight: true, WorkItemIteration: true, + WorkItemHealthStatus: true, + WorkItemDetailModal: stubComponent(WorkItemDetailModal, { + methods: { + show: showModalHandler, + }, + }), }, }); }; @@ -652,15 +664,89 @@ describe('WorkItemDetail component', () => { expect(findHierarchyTree().exists()).toBe(false); }); - it('renders children tree when work item is an Objective', async () => { + describe('work item has children', () => { const objectiveWorkItem = workItemResponseFactory({ workItemType: objectiveType, + confidential: true, }); const handler = jest.fn().mockResolvedValue(objectiveWorkItem); - createComponent({ handler }); + + it('renders children tree when work item is an Objective', async () => { + createComponent({ handler }); + await waitForPromises(); + + expect(findHierarchyTree().exists()).toBe(true); + }); + + it('renders a modal', async () => { + createComponent({ handler }); + await waitForPromises(); + + expect(findModal().exists()).toBe(true); + }); + + it('opens the modal with the child when `show-modal` is emitted', async () => { + createComponent({ handler }); + await waitForPromises(); + + const event = { + preventDefault: jest.fn(), + }; + + findHierarchyTree().vm.$emit('show-modal', event, { id: 'childWorkItemId' }); + await waitForPromises(); + + expect(wrapper.findComponent(WorkItemDetailModal).props().workItemId).toBe( + 'childWorkItemId', + ); + expect(showModalHandler).toHaveBeenCalled(); + }); + + describe('work item is rendered in a modal and has children', () => { + beforeEach(async () => { + createComponent({ + isModal: true, + handler, + }); + + 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(), + }; + + findHierarchyTree().vm.$emit('show-modal', event, { id: 'childWorkItemId' }); + await waitForPromises(); + + expect(wrapper.emitted('update-modal')).toBeDefined(); + }); + }); + }); + }); + + describe('notes widget', () => { + it('does not render notes by default', async () => { + createComponent(); + await waitForPromises(); + + expect(findNotesWidget().exists()).toBe(false); + }); + + it('renders notes when the work_items_mvc flag is on', async () => { + const notesWorkItem = workItemResponseFactory({ + notesWidgetPresent: true, + }); + const handler = jest.fn().mockResolvedValue(notesWorkItem); + createComponent({ workItemsMvcEnabled: true, handler }); await waitForPromises(); - expect(findHierarchyTree().exists()).toBe(true); + expect(findNotesWidget().exists()).toBe(true); }); }); }); 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} diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index ed68d214fc9..23dd2b6bacb 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -1,18 +1,22 @@ import { GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import SystemNote from '~/work_items/components/notes/system_note.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; +import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue'; +import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql'; -import { WIDGET_TYPE_NOTES } from '~/work_items/constants'; +import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants'; +import { DESC } from '~/notes/constants'; import { mockWorkItemNotesResponse, workItemQueryResponse, mockWorkItemNotesByIidResponse, + mockMoreWorkItemNotesResponse, } from '../mock_data'; const mockWorkItemId = workItemQueryResponse.data.workItem.id; @@ -24,6 +28,12 @@ const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspa (widget) => widget.type === WIDGET_TYPE_NOTES, ); +const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem.widgets.find( + (widget) => widget.type === WIDGET_TYPE_NOTES, +); + +const firstSystemNodeId = mockNotesWidgetResponse.discussions.nodes[0].notes.nodes[0].id; + describe('WorkItemNotes component', () => { let wrapper; @@ -31,16 +41,24 @@ describe('WorkItemNotes component', () => { const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote); const findActivityLabel = () => wrapper.find('label'); + const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findSortingFilter = () => wrapper.findComponent(ActivityFilter); + const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index); const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse); const workItemNotesByIidQueryHandler = jest .fn() .mockResolvedValue(mockWorkItemNotesByIidResponse); + const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse); - const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false } = {}) => { + const createComponent = ({ + workItemId = mockWorkItemId, + fetchByIid = false, + defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler, + } = {}) => { wrapper = shallowMount(WorkItemNotes, { apolloProvider: createMockApollo([ - [workItemNotesQuery, workItemNotesQueryHandler], + [workItemNotesQuery, defaultWorkItemNotesQueryHandler], [workItemNotesByIidQuery, workItemNotesByIidQueryHandler], ]), propsData: { @@ -50,6 +68,7 @@ describe('WorkItemNotes component', () => { }, fullPath: 'test-path', fetchByIid, + workItemType: 'task', }, provide: { glFeatures: { @@ -63,14 +82,17 @@ describe('WorkItemNotes component', () => { createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders activity label', () => { expect(findActivityLabel().exists()).toBe(true); }); + it('passes correct props to comment form component', async () => { + createComponent({ workItemId: mockWorkItemId, fetchByIid: false }); + await waitForPromises(); + + expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false); + }); + describe('when notes are loading', () => { it('renders skeleton loader', () => { expect(findSkeletonLoader().exists()).toBe(true); @@ -98,10 +120,65 @@ describe('WorkItemNotes component', () => { await waitForPromises(); }); - it('shows the notes list', () => { + it('renders the notes list to the length of the response', () => { expect(findAllSystemNotes()).toHaveLength( mockNotesByIidWidgetResponse.discussions.nodes.length, ); }); + + it('passes correct props to comment form component', () => { + expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true); + }); + }); + + describe('Pagination', () => { + describe('When there is no next page', () => { + it('fetch more notes is not called', async () => { + createComponent(); + await nextTick(); + expect(workItemMoreNotesQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when there is next page', () => { + beforeEach(async () => { + createComponent({ defaultWorkItemNotesQueryHandler: workItemMoreNotesQueryHandler }); + await waitForPromises(); + }); + + it('fetch more notes should be called', async () => { + expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({ + pageSize: DEFAULT_PAGE_SIZE_NOTES, + id: 'gid://gitlab/WorkItem/1', + }); + + await nextTick(); + + expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({ + pageSize: 45, + id: 'gid://gitlab/WorkItem/1', + after: mockMoreNotesWidgetResponse.discussions.pageInfo.endCursor, + }); + }); + }); + }); + + describe('Sorting', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('filter exists', () => { + expect(findSortingFilter().exists()).toBe(true); + }); + + it('sorts the list when the `changeSortOrder` event is emitted', async () => { + expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId); + + await findSortingFilter().vm.$emit('changeSortOrder', DESC); + + expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId); + }); }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 850672b68d0..67b477b6eb0 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -62,6 +62,7 @@ export const workItemQueryResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, workItemType: { __typename: 'WorkItemType', @@ -156,6 +157,7 @@ export const updateWorkItemMutationResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, workItemType: { __typename: 'WorkItemType', @@ -268,6 +270,7 @@ export const workItemResponseFactory = ({ milestoneWidgetPresent = true, iterationWidgetPresent = true, healthStatusWidgetPresent = true, + notesWidgetPresent = true, confidential = false, canInviteMembers = false, allowsScopedLabels = false, @@ -292,6 +295,7 @@ export const workItemResponseFactory = ({ __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, workItemType, userPermissions: { @@ -380,6 +384,23 @@ export const workItemResponseFactory = ({ healthStatus: 'onTrack', } : { type: 'MOCK TYPE' }, + notesWidgetPresent + ? { + __typename: 'WorkItemWidgetNotes', + type: 'NOTES', + discussions: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: null, + endCursor: + 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0xNCAwNDoxOTowMC4wOTkxMTcwMDAgKzAwMDAiLCJpZCI6IjQyNyIsIl9rZCI6Im4ifQ==', + __typename: 'PageInfo', + }, + nodes: [], + }, + } + : { type: 'MOCK TYPE' }, { __typename: 'WorkItemWidgetHierarchy', type: 'HIERARCHY', @@ -409,6 +430,12 @@ export const workItemResponseFactory = ({ }, parent, }, + notesWidgetPresent + ? { + __typename: 'WorkItemWidgetNotes', + type: 'NOTES', + } + : { type: 'MOCK TYPE' }, ], }, }, @@ -448,6 +475,7 @@ export const createWorkItemMutationResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, workItemType: { __typename: 'WorkItemType', @@ -485,6 +513,7 @@ export const createWorkItemFromTaskMutationResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, workItemType: { __typename: 'WorkItemType', @@ -524,6 +553,7 @@ export const createWorkItemFromTaskMutationResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, workItemType: { __typename: 'WorkItemType', @@ -698,6 +728,20 @@ export const workItemIterationSubscriptionResponse = { }, }; +export const workItemHealthStatusSubscriptionResponse = { + data: { + issuableHealthStatusUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetHealthStatus', + healthStatus: 'needsAttention', + }, + ], + }, + }, +}; + export const workItemMilestoneSubscriptionResponse = { data: { issuableMilestoneUpdated: { @@ -734,6 +778,7 @@ export const workItemHierarchyEmptyResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, userPermissions: { deleteWorkItem: false, @@ -780,6 +825,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, confidential: false, widgets: [ @@ -920,6 +966,7 @@ export const workItemHierarchyResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, widgets: [ { @@ -942,6 +989,43 @@ export const workItemHierarchyResponse = { }, }; +export const workItemObjectiveMetadataWidgets = { + ASSIGNEES: { + type: 'ASSIGNEES', + __typename: 'WorkItemWidgetAssignees', + canInviteMembers: true, + allowsMultipleAssignees: true, + assignees: { + __typename: 'UserCoreConnection', + nodes: mockAssignees, + }, + }, + HEALTH_STATUS: { + type: 'HEALTH_STATUS', + __typename: 'WorkItemWidgetHealthStatus', + healthStatus: 'onTrack', + }, + LABELS: { + type: 'LABELS', + __typename: 'WorkItemWidgetLabels', + allowsScopedLabels: true, + labels: { + __typename: 'LabelConnection', + nodes: mockLabels, + }, + }, + MILESTONE: { + type: 'MILESTONE', + __typename: 'WorkItemWidgetMilestone', + milestone: mockMilestone, + }, + PROGRESS: { + type: 'PROGRESS', + __typename: 'WorkItemWidgetProgress', + progress: 10, + }, +}; + export const workItemObjectiveWithChild = { id: 'gid://gitlab/WorkItem/12', iid: '12', @@ -955,6 +1039,7 @@ export const workItemObjectiveWithChild = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, userPermissions: { deleteWorkItem: true, @@ -976,30 +1061,11 @@ export const workItemObjectiveWithChild = { }, __typename: 'WorkItemWidgetHierarchy', }, - { - type: 'MILESTONE', - __typename: 'WorkItemWidgetMilestone', - milestone: mockMilestone, - }, - { - type: 'ASSIGNEES', - __typename: 'WorkItemWidgetAssignees', - canInviteMembers: true, - allowsMultipleAssignees: true, - assignees: { - __typename: 'UserCoreConnection', - nodes: mockAssignees, - }, - }, - { - type: 'LABELS', - __typename: 'WorkItemWidgetLabels', - allowsScopedLabels: true, - labels: { - __typename: 'LabelConnection', - nodes: mockLabels, - }, - }, + workItemObjectiveMetadataWidgets.PROGRESS, + workItemObjectiveMetadataWidgets.HEALTH_STATUS, + workItemObjectiveMetadataWidgets.MILESTONE, + workItemObjectiveMetadataWidgets.ASSIGNEES, + workItemObjectiveMetadataWidgets.LABELS, ], __typename: 'WorkItem', }; @@ -1012,6 +1078,16 @@ export const workItemObjectiveNoMetadata = { hasChildren: true, __typename: 'WorkItemWidgetHierarchy', }, + { + __typename: 'WorkItemWidgetProgress', + type: 'PROGRESS', + progress: null, + }, + { + __typename: 'WorkItemWidgetMilestone', + type: 'MILESTONE', + milestone: null, + }, ], }; @@ -1036,6 +1112,7 @@ export const workItemHierarchyTreeResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, widgets: [ { @@ -1118,6 +1195,7 @@ export const changeWorkItemParentMutationResponse = { __typename: 'Project', id: '1', fullPath: 'test-project-path', + archived: false, }, widgets: [ { @@ -1149,6 +1227,7 @@ export const availableWorkItemsResponse = { title: 'Task 1', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', + confidential: false, __typename: 'WorkItem', }, { @@ -1156,6 +1235,15 @@ export const availableWorkItemsResponse = { title: 'Task 2', state: 'OPEN', createdAt: '2022-08-03T12:41:54Z', + confidential: false, + __typename: 'WorkItem', + }, + { + id: 'gid://gitlab/WorkItem/460', + title: 'Task 3', + state: 'OPEN', + createdAt: '2022-08-03T12:41:54Z', + confidential: true, __typename: 'WorkItem', }, ], @@ -1514,11 +1602,16 @@ export const mockWorkItemNotesResponse = { nodes: [ { id: 'gid://gitlab/Note/2428', - body: 'added #31 as parent issue', bodyHtml: '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>', systemNoteIconName: 'link', createdAt: '2022-11-14T04:18:59Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, author: { avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', @@ -1541,12 +1634,17 @@ export const mockWorkItemNotesResponse = { notes: { nodes: [ { - id: 'gid://gitlab/MilestoneNote/not-persisted', - body: 'changed milestone to %5', + id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864', bodyHtml: '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>', systemNoteIconName: 'clock', createdAt: '2022-11-14T04:18:59Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, author: { avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', @@ -1569,11 +1667,16 @@ export const mockWorkItemNotesResponse = { notes: { nodes: [ { - id: 'gid://gitlab/WeightNote/not-persisted', - body: 'changed weight to 89', + id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864', bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>', systemNoteIconName: 'weight', createdAt: '2022-11-25T07:16:20Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, author: { avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', @@ -1656,11 +1759,16 @@ export const mockWorkItemNotesByIidResponse = { nodes: [ { id: 'gid://gitlab/Note/2428', - body: 'added #31 as parent issue', bodyHtml: '\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e', systemNoteIconName: 'link', createdAt: '2022-11-14T04:18:59Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, author: { id: 'gid://gitlab/User/1', avatarUrl: @@ -1685,11 +1793,16 @@ export const mockWorkItemNotesByIidResponse = { { id: 'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc', - body: 'changed milestone to %5', bodyHtml: '\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e', systemNoteIconName: 'clock', createdAt: '2022-11-14T04:18:59Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, author: { id: 'gid://gitlab/User/1', avatarUrl: @@ -1714,11 +1827,16 @@ export const mockWorkItemNotesByIidResponse = { { id: 'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3', - body: 'changed iteration to *iteration:5352', bodyHtml: '\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e', systemNoteIconName: 'iteration', createdAt: '2022-11-14T04:19:00Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, author: { id: 'gid://gitlab/User/1', avatarUrl: @@ -1750,3 +1868,183 @@ export const mockWorkItemNotesByIidResponse = { }, }, }; +export const mockMoreWorkItemNotesResponse = { + data: { + workItem: { + id: 'gid://gitlab/WorkItem/600', + iid: '60', + widgets: [ + { + __typename: 'WorkItemWidgetIteration', + }, + { + __typename: 'WorkItemWidgetWeight', + }, + { + __typename: 'WorkItemWidgetAssignees', + }, + { + __typename: 'WorkItemWidgetLabels', + }, + { + __typename: 'WorkItemWidgetDescription', + }, + { + __typename: 'WorkItemWidgetHierarchy', + }, + { + __typename: 'WorkItemWidgetStartAndDueDate', + }, + { + __typename: 'WorkItemWidgetMilestone', + }, + { + type: 'NOTES', + discussions: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + startCursor: null, + endCursor: 'endCursor', + __typename: 'PageInfo', + }, + nodes: [ + { + id: + 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e', + notes: { + nodes: [ + { + id: 'gid://gitlab/Note/2428', + bodyHtml: + '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>', + systemNoteIconName: 'link', + createdAt: '2022-11-14T04:18:59Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + { + id: + 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc', + notes: { + nodes: [ + { + id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823', + bodyHtml: + '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>', + systemNoteIconName: 'clock', + createdAt: '2022-11-14T04:18:59Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + { + id: + 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864', + notes: { + nodes: [ + { + id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864', + bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>', + systemNoteIconName: 'weight', + createdAt: '2022-11-25T07:16:20Z', + system: true, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + ], + __typename: 'DiscussionConnection', + }, + __typename: 'WorkItemWidgetNotes', + }, + ], + __typename: 'WorkItem', + }, + }, +}; + +export const createWorkItemNoteResponse = { + data: { + createNote: { + errors: [], + __typename: 'CreateNotePayload', + }, + }, +}; + +export const mockWorkItemCommentNote = { + id: 'gid://gitlab/Note/158', + bodyHtml: + '<p data-sourcepos="1:1-1:76" dir="auto"><gl-emoji title="waving hand sign" data-name="wave" data-unicode-version="6.0">👋</gl-emoji> Hi <a href="/fredda.brekke" data-reference-type="user" data-user="3" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Sherie Nitzsche">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji title="person with folded hands" data-name="pray" data-unicode-version="6.0">🙏</gl-emoji></p>', + systemNoteIconName: false, + createdAt: '2022-11-25T07:16:20Z', + system: false, + internal: false, + userPermissions: { + adminNote: false, + __typename: 'NotePermissions', + }, + author: { + avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + __typename: 'UserCore', + }, +}; diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index b503d819435..ef9ae4a2eab 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -74,6 +74,7 @@ describe('Work items router', () => { stubs: { WorkItemWeight: true, WorkItemIteration: true, + WorkItemHealthStatus: true, }, }); }; |