Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/work_items')
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap9
-rw-r--r--spec/frontend/work_items/components/notes/activity_filter_spec.js74
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_body_spec.js32
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_comment_form_spec.js205
-rw-r--r--spec/frontend/work_items/components/work_item_comment_locked_spec.js41
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js18
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js92
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js27
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js21
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js93
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js29
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js95
-rw-r--r--spec/frontend/work_items/mock_data.js362
-rw-r--r--spec/frontend/work_items/router_spec.js1
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,
},
});
};