summaryrefslogtreecommitdiff
path: root/spec/frontend/work_items
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 13:49:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 13:49:51 +0000
commit71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch)
tree6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/frontend/work_items
parenta7253423e3403b8c08f8a161e5937e1488f5f407 (diff)
downloadgitlab-ce-a36f25615e8226344d87b692ccf3e543d5d81712.tar.gz
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/frontend/work_items')
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap3
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js (renamed from spec/frontend/work_items/components/work_item_comment_form_spec.js)123
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js164
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js (renamed from spec/frontend/work_items/components/work_item_comment_locked_spec.js)2
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js149
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js52
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_replying_spec.js34
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js256
-rw-r--r--spec/frontend/work_items/components/widget_wrapper_spec.js46
-rw-r--r--spec/frontend/work_items/components/work_item_created_updated_spec.js104
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js44
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js103
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js171
-rw-r--r--spec/frontend/work_items/mock_data.js443
-rw-r--r--spec/frontend/work_items/utils_spec.js27
21 files changed, 1636 insertions, 202 deletions
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
new file mode 100644
index 00000000000..5901642b8a1
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\"></note-header-stub>"`;
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
index 3e3b8bf65b2..fd5f373d076 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -6,6 +6,7 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -95,7 +96,7 @@ describe('system note component', () => {
it.skip('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
- .reply(200, [
+ .reply(HTTP_STATUS_OK, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 07c00119398..2a65e91a906 100644
--- a/spec/frontend/work_items/components/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -5,21 +5,23 @@ 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 { clearDraft } from '~/lib/utils/autosave';
+import { config } from '~/graphql_shared/issuable_client';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
+import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import createNoteMutation from '~/work_items/graphql/notes/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 workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
workItemResponseFactory,
workItemQueryResponse,
projectWorkItemResponse,
createWorkItemNoteResponse,
-} from '../mock_data';
+ mockWorkItemNotesResponse,
+} from '../../mock_data';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/lib/utils/autosave');
@@ -35,18 +37,7 @@ describe('WorkItemCommentForm', () => {
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 findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
@@ -56,6 +47,7 @@ describe('WorkItemCommentForm', () => {
fetchByIid = false,
signedIn = true,
isEditing = true,
+ workItemType = 'Task',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
@@ -64,21 +56,36 @@ describe('WorkItemCommentForm', () => {
window.gon.current_user_avatar_url = 'avatar.png';
}
- const { id } = workItemQueryResponse.data.workItem;
- wrapper = shallowMount(WorkItemCommentForm, {
- apolloProvider: createMockApollo([
+ const apolloProvider = createMockApollo(
+ [
[workItemQuery, workItemResponseHandler],
[createNoteMutation, mutationHandler],
[workItemByIidQuery, workItemByIidResponseHandler],
- ]),
+ ],
+ {},
+ { ...config.cacheConfig },
+ );
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesQuery,
+ variables: {
+ id: workItemId,
+ pageSize: 100,
+ },
+ data: mockWorkItemNotesResponse.data,
+ });
+
+ const { id } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemAddNote, {
+ apolloProvider,
propsData: {
workItemId: id,
fullPath: 'test-project-path',
queryVariables,
fetchByIid,
+ workItemType,
},
stubs: {
- MarkdownField,
WorkItemCommentLocked,
},
});
@@ -99,9 +106,7 @@ describe('WorkItemCommentForm', () => {
signedIn: true,
});
- setText(noteText);
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', noteText);
await waitForPromises();
@@ -109,6 +114,7 @@ describe('WorkItemCommentForm', () => {
input: {
noteableId: workItemId,
body: noteText,
+ discussionId: null,
},
});
});
@@ -117,9 +123,7 @@ describe('WorkItemCommentForm', () => {
await createComponent();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- setText('test');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'test');
await waitForPromises();
@@ -130,6 +134,33 @@ describe('WorkItemCommentForm', () => {
});
});
+ it('emits `replied` event and hides form after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ queryVariables: {
+ id: mockWorkItemNotesResponse.data.workItem.id,
+ },
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'some text');
+ await waitForPromises();
+
+ expect(wrapper.emitted('replied')).toEqual([[]]);
+ });
+
+ it('clears a draft after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'some text');
+ await waitForPromises();
+
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ });
+
it('emits error when mutation returns error', async () => {
const error = 'eror';
@@ -138,16 +169,26 @@ describe('WorkItemCommentForm', () => {
mutationHandler: jest.fn().mockResolvedValue({
data: {
createNote: {
- note: null,
+ note: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ __typename: 'CreateNotePayload',
errors: [error],
},
},
}),
});
- setText('updated desc');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
await waitForPromises();
@@ -162,24 +203,12 @@ describe('WorkItemCommentForm', () => {
mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
});
- setText('updated desc');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
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 () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
new file mode 100644
index 00000000000..23a9f285804
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -0,0 +1,164 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as autosave from '~/lib/utils/autosave';
+import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
+import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+
+const draftComment = 'draft comment';
+
+jest.mock('~/lib/utils/autosave', () => ({
+ updateDraft: jest.fn(),
+ clearDraft: jest.fn(),
+ getDraft: jest.fn().mockReturnValue(draftComment),
+}));
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
+ confirmAction: jest.fn().mockResolvedValue(true),
+}));
+
+describe('Work item comment form component', () => {
+ let wrapper;
+
+ const mockAutosaveKey = 'test-auto-save-key';
+
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
+ const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
+
+ const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
+ wrapper = shallowMount(WorkItemCommentForm, {
+ propsData: {
+ workItemType: 'Issue',
+ ariaLabel: 'test-aria-label',
+ autosaveKey: mockAutosaveKey,
+ isSubmitting,
+ initialValue,
+ },
+ provide: {
+ fullPath: 'test-project-path',
+ },
+ });
+ };
+
+ it('passes correct markdown preview path to markdown editor', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('renderMarkdownPath')).toBe(
+ '/test-project-path/preview_markdown?target_type=Issue',
+ );
+ });
+
+ it('passes correct form field props to markdown editor', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('formFieldProps')).toEqual({
+ 'aria-label': 'test-aria-label',
+ id: 'work-item-add-or-edit-comment',
+ name: 'work-item-add-or-edit-comment',
+ placeholder: 'Write a comment or drag your files here…',
+ });
+ });
+
+ it('passes correct `loading` prop to confirm button', () => {
+ createComponent({ isSubmitting: true });
+
+ expect(findConfirmButton().props('loading')).toBe(true);
+ });
+
+ it('passes a draft from local storage as a value to markdown editor if the draft exists', () => {
+ createComponent({ initialValue: 'parent comment' });
+ expect(findMarkdownEditor().props('value')).toBe(draftComment);
+ });
+
+ it('passes an initialValue prop as a value to markdown editor if storage draft does not exist', () => {
+ jest.spyOn(autosave, 'getDraft').mockImplementation(() => '');
+ createComponent({ initialValue: 'parent comment' });
+
+ expect(findMarkdownEditor().props('value')).toBe('parent comment');
+ });
+
+ it('passes an empty string as a value to markdown editor if storage draft and initialValue are empty', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('value')).toBe('');
+ });
+
+ describe('on markdown editor input', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets correct comment text value', async () => {
+ expect(findMarkdownEditor().props('value')).toBe('');
+
+ findMarkdownEditor().vm.$emit('input', 'new comment');
+ await nextTick();
+
+ expect(findMarkdownEditor().props('value')).toBe('new comment');
+ });
+
+ it('calls `updateDraft` with correct parameters', async () => {
+ findMarkdownEditor().vm.$emit('input', 'new comment');
+
+ expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment');
+ });
+ });
+
+ describe('on cancel editing', () => {
+ beforeEach(() => {
+ jest.spyOn(autosave, 'getDraft').mockImplementation(() => draftComment);
+ createComponent();
+ findMarkdownEditor().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY }));
+
+ return waitForPromises();
+ });
+
+ it('confirms a user action if comment text is not empty', () => {
+ expect(confirmViaGlModal.confirmAction).toHaveBeenCalled();
+ });
+
+ it('emits `cancelEditing` and clears draft from the local storage', () => {
+ expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
+ expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
+ });
+ });
+
+ it('cancels editing on clicking cancel button', async () => {
+ createComponent();
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
+ expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
+ });
+
+ it('emits `submitForm` event on confirm button click', () => {
+ createComponent();
+ findConfirmButton().vm.$emit('click');
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+
+ it('emits `submitForm` event on pressing enter with meta key on markdown editor', () => {
+ createComponent();
+ findMarkdownEditor().vm.$emit(
+ 'keydown',
+ new KeyboardEvent('keydown', { key: ENTER_KEY, metaKey: true }),
+ );
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+
+ it('emits `submitForm` event on pressing ctrl+enter on markdown editor', () => {
+ createComponent();
+ findMarkdownEditor().vm.$emit(
+ 'keydown',
+ new KeyboardEvent('keydown', { key: ENTER_KEY, ctrlKey: true }),
+ );
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_comment_locked_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js
index 58491c4b09c..734b474c8fc 100644
--- a/spec/frontend/work_items/components/work_item_comment_locked_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js
@@ -1,6 +1,6 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
+import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
const createComponent = ({ workItemType = 'Task', isProjectArchived = false } = {}) =>
shallowMount(WorkItemCommentLocked, {
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
new file mode 100644
index 00000000000..bb65b75c4d8
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -0,0 +1,149 @@
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
+import {
+ mockWorkItemCommentNote,
+ mockWorkItemNotesResponseWithComments,
+} from 'jest/work_items/mock_data';
+import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+describe('Work Item Discussion', () => {
+ let wrapper;
+ const mockWorkItemId = 'gid://gitlab/WorkItem/625';
+
+ const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
+ const findAllThreads = () => wrapper.findAllComponents(WorkItemNote);
+ const findThreadAtIndex = (index) => findAllThreads().at(index);
+ const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
+ const findWorkItemNoteReplying = () => wrapper.findComponent(WorkItemNoteReplying);
+
+ const createComponent = ({
+ discussion = [mockWorkItemCommentNote],
+ workItemId = mockWorkItemId,
+ queryVariables = { id: workItemId },
+ fetchByIid = false,
+ fullPath = 'gitlab-org',
+ workItemType = 'Task',
+ } = {}) => {
+ wrapper = shallowMount(WorkItemDiscussion, {
+ propsData: {
+ discussion,
+ workItemId,
+ queryVariables,
+ fetchByIid,
+ fullPath,
+ workItemType,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ 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('should not show the the toggle replies widget wrapper when no replies', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(false);
+ });
+
+ it('should not show the comment form by default', () => {
+ expect(findWorkItemAddNote().exists()).toBe(false);
+ });
+ });
+
+ describe('When the main comments has threads', () => {
+ beforeEach(() => {
+ createComponent({
+ discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
+ });
+ });
+
+ it('should show the toggle replies widget', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ });
+
+ it('the number of threads should be equal to the response length', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAllThreads()).toHaveLength(
+ mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length,
+ );
+ });
+
+ it('should autofocus when we click expand replies', async () => {
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ expect(findWorkItemAddNote().exists()).toBe(true);
+ expect(findWorkItemAddNote().props('autofocus')).toBe(true);
+ });
+ });
+
+ describe('When replying to any comment', () => {
+ beforeEach(async () => {
+ createComponent({
+ discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
+ });
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ await findWorkItemAddNote().vm.$emit('replying', 'reply text');
+ });
+
+ it('should show optimistic behavior when replying', async () => {
+ expect(findAllThreads()).toHaveLength(2);
+ expect(findWorkItemNoteReplying().exists()).toBe(true);
+ });
+
+ it('should be expanded when the reply is successful', async () => {
+ findWorkItemAddNote().vm.$emit('replied');
+ await nextTick();
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
+ });
+ });
+
+ it('emits `deleteNote` event with correct parameter when child note component emits `deleteNote` event', () => {
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('deleteNote');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]);
+ });
+
+ it('emits `error` event when child note emits an `error`', () => {
+ const mockErrorText = 'Houston, we have a problem';
+
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('error', mockErrorText);
+
+ expect(wrapper.emitted('error')).toEqual([[mockErrorText]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
new file mode 100644
index 00000000000..d85cd46c1c3
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount } from '@vue/test-utils';
+import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+
+describe('Work Item Note Actions', () => {
+ let wrapper;
+
+ const findReplyButton = () => wrapper.findComponent(ReplyButton);
+ const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
+
+ const createComponent = ({ showReply = true, showEdit = true } = {}) => {
+ wrapper = shallowMount(WorkItemNoteActions, {
+ propsData: {
+ showReply,
+ showEdit,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ it('Should show the reply button by default', () => {
+ createComponent();
+ expect(findReplyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('When the reply button needs to be hidden', () => {
+ it('Should show the reply button by default', () => {
+ createComponent({ showReply: false });
+ expect(findReplyButton().exists()).toBe(false);
+ });
+ });
+
+ it('shows edit button when `showEdit` prop is true', () => {
+ createComponent();
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('does not show edit button when `showEdit` prop is false', () => {
+ createComponent({ showEdit: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('emits `startEditing` event when edit button is clicked', () => {
+ createComponent();
+ findEditButton().vm.$emit('click');
+
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js b/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js
new file mode 100644
index 00000000000..225cc3bacaf
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe('Work Item Note Replying', () => {
+ let wrapper;
+ const mockNoteBody = 'replying body';
+
+ const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
+ const findNoteHeader = () => wrapper.findComponent(NoteHeader);
+
+ const createComponent = ({ body = mockNoteBody } = {}) => {
+ wrapper = shallowMount(WorkItemNoteReplying, {
+ propsData: {
+ body,
+ },
+ });
+
+ window.gon.current_user_id = '1';
+ window.gon.current_user_avatar_url = 'avatar.png';
+ window.gon.current_user_fullname = 'Administrator';
+ window.gon.current_username = 'user';
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the note body and header', () => {
+ expect(findTimelineEntry().exists()).toBe(true);
+ expect(findNoteHeader().html()).toMatchSnapshot();
+ });
+});
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
index 7257d5c8023..9b87419cee7 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,53 +1,261 @@
-import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import mockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateDraft } from '~/lib/utils/autosave';
+import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
+import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+Vue.use(VueApollo);
+jest.mock('~/lib/utils/autosave');
+
describe('Work Item Note', () => {
let wrapper;
+ const updatedNoteText = '# Some title';
+ const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
+
+ const successHandler = jest.fn().mockResolvedValue({
+ data: {
+ updateNote: {
+ errors: [],
+ note: {
+ ...mockWorkItemCommentNote,
+ body: updatedNoteText,
+ bodyHtml: updatedNoteBody,
+ },
+ },
+ },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Oops');
+ const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
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 findNoteActions = () => wrapper.findComponent(NoteActions);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findEditedAt = () => wrapper.findComponent(EditedAt);
- const createComponent = ({ note = mockWorkItemCommentNote } = {}) => {
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
+ const findNoteWrapper = () => wrapper.find('[data-testid="note-wrapper"]');
+
+ const createComponent = ({
+ note = mockWorkItemCommentNote,
+ isFirstNote = false,
+ updateNoteMutationHandler = successHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemNote, {
propsData: {
note,
+ isFirstNote,
+ workItemType: 'Task',
},
+ apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
});
};
- beforeEach(() => {
- createComponent();
- });
+ describe('when editing', () => {
+ beforeEach(() => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ return nextTick();
+ });
- it('Should be wrapped inside the timeline entry item', () => {
- expect(findTimelineEntryItem().exists()).toBe(true);
- });
+ it('should render a comment form', () => {
+ expect(findCommentForm().exists()).toBe(true);
+ });
+
+ it('should not render note wrapper', () => {
+ expect(findNoteWrapper().exists()).toBe(false);
+ });
+
+ it('updates saved draft with current note text', () => {
+ expect(updateDraft).toHaveBeenCalledWith(
+ `${mockWorkItemCommentNote.id}-comment`,
+ mockWorkItemCommentNote.body,
+ );
+ });
- it('should have the author avatar of the work item note', () => {
- expect(findAvatarLink().exists()).toBe(true);
- expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
+ it('passes correct autosave key prop to comment form component', () => {
+ expect(findCommentForm().props('autosaveKey')).toBe(`${mockWorkItemCommentNote.id}-comment`);
+ });
+
+ it('should hide a form and show wrapper when user cancels editing', async () => {
+ findCommentForm().vm.$emit('cancelEditing');
+ await nextTick();
- expect(findAvatar().exists()).toBe(true);
- expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
- expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
+ expect(findCommentForm().exists()).toBe(false);
+ expect(findNoteWrapper().exists()).toBe(true);
+ });
});
- it('has note header', () => {
- expect(findNoteHeader().exists()).toBe(true);
- expect(findNoteHeader().props('author')).toEqual(mockWorkItemCommentNote.author);
- expect(findNoteHeader().props('createdAt')).toBe(mockWorkItemCommentNote.createdAt);
+ describe('when submitting a form to edit a note', () => {
+ it('calls update mutation with correct variables', async () => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItemCommentNote.id,
+ body: updatedNoteText,
+ },
+ });
+ });
+
+ it('hides the form after succesful mutation', async () => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ await waitForPromises();
+
+ expect(findCommentForm().exists()).toBe(false);
+ });
+
+ describe('when mutation fails', () => {
+ beforeEach(async () => {
+ createComponent({ updateNoteMutationHandler: errorHandler });
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ await waitForPromises();
+ });
+
+ it('opens the form again', () => {
+ expect(findCommentForm().exists()).toBe(true);
+ });
+
+ it('updates the saved draft with the latest comment text', () => {
+ expect(updateDraft).toHaveBeenCalledWith(
+ `${mockWorkItemCommentNote.id}-comment`,
+ updatedNoteText,
+ );
+ });
+
+ it('emits an error', () => {
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
});
- it('has note body', () => {
- expect(findNoteBody().exists()).toBe(true);
- expect(findNoteBody().props('note')).toEqual(mockWorkItemCommentNote);
+ describe('when not editing', () => {
+ it('should not render a comment form', () => {
+ createComponent();
+ expect(findCommentForm().exists()).toBe(false);
+ });
+
+ it('should render note wrapper', () => {
+ createComponent();
+ expect(findNoteWrapper().exists()).toBe(true);
+ });
+
+ it('renders no "edited at" information by default', () => {
+ createComponent();
+ expect(findEditedAt().exists()).toBe(false);
+ });
+
+ it('renders "edited at" information if the note was edited', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ lastEditedAt: '2023-02-12T07:47:40Z',
+ lastEditedBy: { ...mockWorkItemCommentNote.author, webPath: 'test-path' },
+ },
+ });
+
+ expect(findEditedAt().exists()).toBe(true);
+ expect(findEditedAt().props()).toEqual({
+ updatedAt: '2023-02-12T07:47:40Z',
+ updatedByName: 'Administrator',
+ updatedByPath: 'test-path',
+ });
+ });
+
+ describe('main comment', () => {
+ beforeEach(() => {
+ createComponent({ isFirstNote: true });
+ });
+
+ it('should have the note header, actions and body', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteActions().exists()).toBe(true);
+ });
+
+ it('should not have the Avatar link for main thread inside the timeline-entry', () => {
+ expect(findAuthorAvatarLink().exists()).toBe(false);
+ });
+
+ it('should have the reply button props', () => {
+ expect(findNoteActions().props('showReply')).toBe(true);
+ });
+ });
+
+ describe('comment threads', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the note header, actions and body', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteActions().exists()).toBe(true);
+ });
+
+ it('should have the Avatar link for comment threads', () => {
+ expect(findAuthorAvatarLink().exists()).toBe(true);
+ });
+
+ it('should not have the reply button props', () => {
+ expect(findNoteActions().props('showReply')).toBe(false);
+ });
+ });
+
+ it('should display a dropdown if user has a permission to delete a note', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('should not display a dropdown if user has no permission to delete a note', () => {
+ createComponent();
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/work_items/components/widget_wrapper_spec.js b/spec/frontend/work_items/components/widget_wrapper_spec.js
new file mode 100644
index 00000000000..a87233300fc
--- /dev/null
+++ b/spec/frontend/work_items/components/widget_wrapper_spec.js
@@ -0,0 +1,46 @@
+import { nextTick } from 'vue';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
+
+describe('WidgetWrapper component', () => {
+ let wrapper;
+
+ const createComponent = ({ error } = {}) => {
+ wrapper = shallowMountExtended(WidgetWrapper, { propsData: { error } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+ const findWidgetBody = () => wrapper.findByTestId('widget-body');
+
+ it('is expanded by default', () => {
+ createComponent();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findWidgetBody().exists()).toBe(true);
+ });
+
+ it('collapses on click toggle button', async () => {
+ createComponent();
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findWidgetBody().exists()).toBe(false);
+ });
+
+ it('shows alert when list loading fails', () => {
+ const error = 'Some error';
+ createComponent({ error });
+
+ expect(findAlert().text()).toBe(error);
+ });
+
+ it('emits event when dismissing the alert', () => {
+ createComponent({ error: 'error' });
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('dismissAlert')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js
new file mode 100644
index 00000000000..fe31c01df36
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js
@@ -0,0 +1,104 @@
+import { GlAvatarLink, GlSprintf } 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 waitForPromises from 'helpers/wait_for_promises';
+import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { workItemResponseFactory, mockAssignees } from '../mock_data';
+
+describe('WorkItemCreatedUpdated component', () => {
+ let wrapper;
+ let successHandler;
+ let successByIidHandler;
+
+ Vue.use(VueApollo);
+
+ const findCreatedAt = () => wrapper.find('[data-testid="work-item-created"]');
+ const findUpdatedAt = () => wrapper.find('[data-testid="work-item-updated"]');
+
+ const findCreatedAtText = () => findCreatedAt().text().replace(/\s+/g, ' ');
+
+ const createComponent = async ({
+ workItemId = 'gid://gitlab/WorkItem/1',
+ workItemIid = '1',
+ fetchByIid = false,
+ author = null,
+ updatedAt,
+ } = {}) => {
+ const workItemQueryResponse = workItemResponseFactory({
+ author,
+ updatedAt,
+ });
+ const byIidResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+ };
+
+ successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ successByIidHandler = jest.fn().mockResolvedValue(byIidResponse);
+
+ const handlers = [
+ [workItemQuery, successHandler],
+ [workItemByIidQuery, successByIidHandler],
+ ];
+
+ wrapper = shallowMount(WorkItemCreatedUpdated, {
+ apolloProvider: createMockApollo(handlers),
+ propsData: { workItemId, workItemIid, fetchByIid, fullPath: '/some/project' },
+ stubs: {
+ GlAvatarLink,
+ GlSprintf,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ describe.each([true, false])('fetchByIid is %s', (fetchByIid) => {
+ describe('work item id and iid undefined', () => {
+ beforeEach(async () => {
+ await createComponent({ workItemId: null, workItemIid: null, fetchByIid });
+ });
+
+ it('skips the work item query', () => {
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ it('shows author name and link', async () => {
+ const author = mockAssignees[0];
+
+ await createComponent({ fetchByIid, author });
+
+ expect(findCreatedAtText()).toEqual(`Created by ${author.name}`);
+ });
+
+ it('shows created time when author is null', async () => {
+ await createComponent({ fetchByIid, author: null });
+
+ expect(findCreatedAtText()).toEqual('Created');
+ });
+
+ it('shows updated time', async () => {
+ await createComponent({ fetchByIid });
+
+ expect(findUpdatedAt().exists()).toBe(true);
+ });
+
+ it('does not show updated time for new work items', async () => {
+ await createComponent({ fetchByIid, updatedAt: null });
+
+ expect(findUpdatedAt().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 05476ef5ca0..a12ec23c15a 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -16,6 +16,7 @@ import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
workItemDescriptionSubscriptionResponse,
@@ -102,6 +103,49 @@ describe('WorkItemDescription', () => {
wrapper.destroy();
});
+ describe('editing description with workItemsMvc FF enabled', () => {
+ beforeEach(() => {
+ workItemsMvc = true;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownEditor().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ supportsQuickActions: true,
+ renderMarkdownPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
+ describe('editing description with workItemsMvc FF disabled', () => {
+ beforeEach(() => {
+ workItemsMvc = false;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ markdownPreviewPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
describe.each([true, false])(
'editing description with workItemsMvc %workItemsMvcEnabled',
(workItemsMvcEnabled) => {
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 8976cd6e22b..938cf6e6f51 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
@@ -136,10 +136,14 @@ describe('WorkItemDetailModal component', () => {
it('updates the work item when WorkItemDetail emits `update-modal` event', async () => {
createComponent();
- findWorkItemDetail().vm.$emit('update-modal', null, 'updatedId');
+ findWorkItemDetail().vm.$emit('update-modal', undefined, {
+ id: 'updatedId',
+ iid: 'updatedIid',
+ });
await waitForPromises();
expect(findWorkItemDetail().props().workItemId).toEqual('updatedId');
+ expect(findWorkItemDetail().props().workItemIid).toEqual('updatedIid');
});
describe('delete work item', () => {
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 a50a48de921..64a7502671e 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -16,6 +16,7 @@ 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';
+import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
@@ -74,6 +75,7 @@ describe('WorkItemDetail component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
+ const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
@@ -92,6 +94,7 @@ describe('WorkItemDetail component', () => {
isModal = false,
updateInProgress = false,
workItemId = workItemQueryResponse.data.workItem.id,
+ workItemIid = '1',
handler = successHandler,
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
@@ -112,7 +115,7 @@ describe('WorkItemDetail component', () => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
- propsData: { isModal, workItemId, workItemIid: '1' },
+ propsData: { isModal, workItemId, workItemIid },
data() {
return {
updateInProgress,
@@ -150,9 +153,9 @@ describe('WorkItemDetail component', () => {
setWindowLocation('');
});
- describe('when there is no `workItemId` prop', () => {
+ describe('when there is no `workItemId` and no `workItemIid` prop', () => {
beforeEach(() => {
- createComponent({ workItemId: null });
+ createComponent({ workItemId: null, workItemIid: null });
});
it('skips the work item query', () => {
@@ -656,6 +659,19 @@ describe('WorkItemDetail component', () => {
});
});
+ it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present and is a modal', async () => {
+ setWindowLocation(`?iid_path=true`);
+
+ createComponent({ fetchByIid: true, iidPathQueryParam: 'true', isModal: true });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'group/project',
+ iid: '1',
+ });
+ });
+
describe('hierarchy widget', () => {
it('does not render children tree by default', async () => {
createComponent();
@@ -686,7 +702,7 @@ describe('WorkItemDetail component', () => {
});
it('opens the modal with the child when `show-modal` is emitted', async () => {
- createComponent({ handler });
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
const event = {
@@ -707,6 +723,7 @@ describe('WorkItemDetail component', () => {
createComponent({
isModal: true,
handler,
+ workItemsMvc2Enabled: true,
});
await waitForPromises();
@@ -749,4 +766,11 @@ describe('WorkItemDetail component', () => {
expect(findNotesWidget().exists()).toBe(true);
});
});
+
+ it('renders created/updated', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findCreatedUpdated().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 083bb5bc4a4..0b6ab5c3290 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -85,7 +85,7 @@ describe('WorkItemLabels component', () => {
it('focuses token selector on token selector input event', async () => {
createComponent();
findTokenSelector().vm.$emit('input', [mockLabels[0]]);
- await nextTick();
+ await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
@@ -189,6 +189,23 @@ describe('WorkItemLabels component', () => {
);
});
+ it('adds new labels to the end', async () => {
+ const response = workItemResponseFactory({ labels: [mockLabels[1]] });
+ const workItemQueryHandler = jest.fn().mockResolvedValue(response);
+ createComponent({
+ workItemQueryHandler,
+ updateWorkItemMutationHandler: successUpdateWorkItemMutationHandler,
+ });
+ await waitForPromises();
+
+ findTokenSelector().vm.$emit('input', [mockLabels[0]]);
+ await waitForPromises();
+
+ const labels = findTokenSelector().props('selectedTokens');
+ expect(labels[0]).toMatchObject(mockLabels[1]);
+ expect(labels[1]).toMatchObject(mockLabels[0]);
+ });
+
describe('when clicking outside the token selector', () => {
it('calls a mutation with correct variables', () => {
createComponent();
@@ -205,9 +222,7 @@ describe('WorkItemLabels component', () => {
});
it('emits an error and resets labels if mutation was rejected', async () => {
- const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
-
- createComponent({ updateWorkItemMutationHandler: errorHandler, workItemQueryHandler });
+ createComponent({ updateWorkItemMutationHandler: errorHandler });
await waitForPromises();
@@ -224,6 +239,23 @@ describe('WorkItemLabels component', () => {
expect(updatedLabels).toEqual(initialLabels);
});
+ it('does not make server request if no labels added or removed', async () => {
+ const updateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
+ createComponent({ updateWorkItemMutationHandler });
+
+ await waitForPromises();
+
+ findTokenSelector().vm.$emit('input', []);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+
it('has a subscription', async () => {
createComponent();
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 5e1c46826cc..480f8fbcc58 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
@@ -40,7 +40,6 @@ describe('WorkItemLinksForm', () => {
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
- workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE,
@@ -62,9 +61,6 @@ describe('WorkItemLinksForm', () => {
formType,
},
provide: {
- glFeatures: {
- workItemsMvc: workItemsMvcEnabled,
- },
projectPath: 'project/path',
hasIterationsFeature,
},
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index a61de78c623..ec51f92b578 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,5 +1,4 @@
import Vue, { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,6 +7,8 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
+import { resolvers } from '~/graphql_shared/issuable_client';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
@@ -17,6 +18,7 @@ import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
+ getIssueDetailsResponse,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
@@ -27,39 +29,6 @@ import {
Vue.use(VueApollo);
-const issueDetailsResponse = (confidential = false) => ({
- data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- issuable: {
- id: 'gid://gitlab/Issue/4',
- confidential,
- iteration: {
- id: 'gid://gitlab/Iteration/1124',
- title: null,
- startDate: '2022-06-22',
- dueDate: '2022-07-19',
- webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
- iterationCadence: {
- id: 'gid://gitlab/Iterations::Cadence/1101',
- title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
- __typename: 'IterationCadence',
- },
- __typename: 'Iteration',
- },
- milestone: {
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/28',
- title: 'v2.0',
- __typename: 'Milestone',
- },
- __typename: 'Issue',
- },
- __typename: 'Project',
- },
- },
-});
const showModal = jest.fn();
describe('WorkItemLinks', () => {
@@ -83,7 +52,7 @@ describe('WorkItemLinks', () => {
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
- issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
fetchByIid = false,
} = {}) => {
@@ -95,7 +64,7 @@ describe('WorkItemLinks', () => {
[issueDetailsQuery, issueDetailsQueryHandler],
[workItemByIidQuery, childWorkItemByIidHandler],
],
- {},
+ resolvers,
{ addTypename: true },
);
@@ -127,12 +96,12 @@ describe('WorkItemLinks', () => {
},
});
+ wrapper.vm.$refs.wrapper.show = jest.fn();
+
await waitForPromises();
};
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findToggleButton = () => wrapper.findByTestId('toggle-links');
- const findLinksBody = () => wrapper.findByTestId('links-body');
+ const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
@@ -142,31 +111,14 @@ describe('WorkItemLinks', () => {
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
- beforeEach(async () => {
- await createComponent();
- });
-
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
setWindowLocation('');
});
- it('is expanded by default', () => {
- expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
- expect(findLinksBody().exists()).toBe(true);
- });
-
- it('collapses on click toggle button', async () => {
- findToggleButton().vm.$emit('click');
- await nextTick();
-
- expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
- expect(findLinksBody().exists()).toBe(false);
- });
-
describe('add link form', () => {
it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
+ await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
@@ -181,6 +133,7 @@ describe('WorkItemLinks', () => {
});
it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
+ await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleCreateFormButton().vm.$emit('click');
await nextTick();
@@ -193,6 +146,24 @@ describe('WorkItemLinks', () => {
expect(findAddLinksForm().exists()).toBe(false);
});
+
+ it('adds work item child from the form', async () => {
+ const workItem = {
+ ...workItemQueryResponse.data.workItem,
+ id: 'gid://gitlab/WorkItem/11',
+ };
+ await createComponent();
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleCreateFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
+
+ findAddLinksForm().vm.$emit('addWorkItemChild', workItem);
+ await waitForPromises();
+
+ expect(findWorkItemLinkChildItems()).toHaveLength(5);
+ });
});
describe('when no child links', () => {
@@ -207,8 +178,8 @@ describe('WorkItemLinks', () => {
});
});
- it('renders all hierarchy widget children', () => {
- expect(findLinksBody().exists()).toBe(true);
+ it('renders all hierarchy widget children', async () => {
+ await createComponent();
expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
@@ -219,15 +190,13 @@ describe('WorkItemLinks', () => {
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
});
- await nextTick();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errorMessage);
+ expect(findWidgetWrapper().props('error')).toBe(errorMessage);
});
- it('displays number if children', () => {
- expect(findChildrenCount().exists()).toBe(true);
+ it('displays number of children', async () => {
+ await createComponent();
+ expect(findChildrenCount().exists()).toBe(true);
expect(findChildrenCount().text()).toContain('4');
});
@@ -294,7 +263,9 @@ describe('WorkItemLinks', () => {
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
- issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ issueDetailsQueryHandler: jest
+ .fn()
+ .mockResolvedValue(getIssueDetailsResponse({ confidential: true })),
});
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
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 156f06a0d5e..0236fe2e60d 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
@@ -23,8 +23,6 @@ describe('WorkItemTree', () => {
let getWorkItemQueryHandler;
let wrapper;
- const findToggleButton = () => wrapper.findByTestId('toggle-tree');
- const findTreeBody = () => wrapper.findByTestId('tree-body');
const findEmptyState = () => wrapper.findByTestId('tree-empty');
const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
@@ -64,36 +62,25 @@ describe('WorkItemTree', () => {
projectPath: 'test/project',
},
});
+
+ wrapper.vm.$refs.wrapper.show = jest.fn();
};
- beforeEach(() => {
+ it('displays Add button', () => {
createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
- it('is expanded by default and displays Add button', () => {
- expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
- expect(findTreeBody().exists()).toBe(true);
expect(findToggleFormSplitButton().exists()).toBe(true);
});
- it('collapses on click toggle button', async () => {
- findToggleButton().vm.$emit('click');
- await nextTick();
-
- expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
- expect(findTreeBody().exists()).toBe(false);
- });
-
it('displays empty state if there are no children', () => {
createComponent({ children: [] });
+
expect(findEmptyState().exists()).toBe(true);
});
it('renders all hierarchy widget children', () => {
+ createComponent();
+
const workItemLinkChildren = findWorkItemLinkChildItems();
expect(workItemLinkChildren).toHaveLength(4);
expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
@@ -102,6 +89,8 @@ describe('WorkItemTree', () => {
});
it('does not display form by default', () => {
+ createComponent();
+
expect(findForm().exists()).toBe(false);
});
@@ -114,6 +103,8 @@ describe('WorkItemTree', () => {
`(
'when selecting $option from split button, renders the form passing $formType and $childType',
async ({ event, formType, childType }) => {
+ createComponent();
+
findToggleFormSplitButton().vm.$emit(event);
await nextTick();
@@ -128,13 +119,16 @@ describe('WorkItemTree', () => {
);
it('remove event on child triggers `removeChild` event', () => {
+ createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
+
firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
});
it('emits `show-modal` on `click` event', () => {
+ createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
const event = {
childItem: 'gid://gitlab/WorkItem/2',
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 23dd2b6bacb..3db848a0ad2 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -1,22 +1,26 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
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 WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.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 workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
-import { DESC } from '~/notes/constants';
+import { ASC, DESC } from '~/notes/constants';
import {
mockWorkItemNotesResponse,
workItemQueryResponse,
mockWorkItemNotesByIidResponse,
mockMoreWorkItemNotesResponse,
+ mockWorkItemNotesResponseWithComments,
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
@@ -32,34 +36,56 @@ const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem.
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
const firstSystemNodeId = mockNotesWidgetResponse.discussions.nodes[0].notes.nodes[0].id;
+const mockDiscussions = mockWorkItemNotesWidgetResponseWithComments.discussions.nodes;
+
describe('WorkItemNotes component', () => {
let wrapper;
Vue.use(VueApollo);
+ const showModal = jest.fn();
+
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
+ const findAllListItems = () => wrapper.findAll('ul.timeline > *');
const findActivityLabel = () => wrapper.find('label');
- const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
+ const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
+ const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
+ const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
+
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
const workItemNotesByIidQueryHandler = jest
.fn()
.mockResolvedValue(mockWorkItemNotesByIidResponse);
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
+ const workItemNotesWithCommentsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesResponseWithComments);
+ const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
workItemId = mockWorkItemId,
fetchByIid = false,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
+ deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
[workItemNotesQuery, defaultWorkItemNotesQueryHandler],
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ [deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
]),
propsData: {
workItemId,
@@ -75,6 +101,9 @@ describe('WorkItemNotes component', () => {
useIidInWorkItemsPath: fetchByIid,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
+ },
});
};
@@ -87,10 +116,14 @@ describe('WorkItemNotes component', () => {
});
it('passes correct props to comment form component', async () => {
- createComponent({ workItemId: mockWorkItemId, fetchByIid: false });
+ createComponent({
+ workItemId: mockWorkItemId,
+ fetchByIid: false,
+ defaultWorkItemNotesQueryHandler: workItemNotesByIidQueryHandler,
+ });
await waitForPromises();
- expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false);
+ expect(findWorkItemAddNote().props('fetchByIid')).toEqual(false);
});
describe('when notes are loading', () => {
@@ -121,13 +154,14 @@ describe('WorkItemNotes component', () => {
});
it('renders the notes list to the length of the response', () => {
+ expect(workItemNotesByIidQueryHandler).toHaveBeenCalled();
expect(findAllSystemNotes()).toHaveLength(
mockNotesByIidWidgetResponse.discussions.nodes.length,
);
});
it('passes correct props to comment form component', () => {
- expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true);
+ expect(findWorkItemAddNote().props('fetchByIid')).toEqual(true);
});
});
@@ -180,5 +214,124 @@ describe('WorkItemNotes component', () => {
expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
});
+
+ it('puts form at start of list in when sorting by newest first', async () => {
+ await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+
+ expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
+ });
+
+ it('puts form at end of list in when sorting by oldest first', async () => {
+ await findSortingFilter().vm.$emit('changeSortOrder', ASC);
+
+ expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
+ });
+ });
+
+ describe('Activity comments', () => {
+ beforeEach(async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+ });
+
+ it('should not have any system notes', () => {
+ expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
+ expect(findAllSystemNotes()).toHaveLength(0);
+ });
+
+ it('should have work item notes', () => {
+ expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(mockDiscussions.length);
+ });
+
+ it('should pass all the correct props to work item comment note', () => {
+ const commentIndex = 0;
+ const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex);
+
+ expect(firstCommentNote.props('discussion')).toEqual(
+ mockDiscussions[commentIndex].notes.nodes,
+ );
+ });
+ });
+
+ it('should open delete modal confirmation when child discussion emits `deleteNote` event', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false });
+ expect(showModal).toHaveBeenCalled();
+ });
+
+ describe('when modal is open', () => {
+ beforeEach(() => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ return waitForPromises();
+ });
+
+ it('sends the mutation with correct variables', () => {
+ const noteId = 'some-test-id';
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: noteId,
+ },
+ });
+ });
+
+ it('successfully removes the note from the discussion', async () => {
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2);
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1);
+ });
+
+ it('successfully removes the discussion from work item if discussion only had one note', async () => {
+ const secondDiscussion = findWorkItemCommentNoteAtIndex(1);
+
+ expect(findAllWorkItemCommentNotes()).toHaveLength(2);
+ expect(secondDiscussion.props('discussion')).toHaveLength(1);
+
+ secondDiscussion.vm.$emit('deleteNote', {
+ id: mockDiscussions[1].notes.nodes[0].id,
+ discussion: { id: mockDiscussions[1].id },
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(1);
+ });
+ });
+
+ it('emits `error` event if delete note mutation is rejected', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ deleteWINoteMutationHandler: errorHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong when deleting a comment. Please try again'],
+ ]);
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 67b477b6eb0..d4832fe376d 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -57,7 +57,16 @@ export const workItemQueryResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ avatarUrl: 'http://127.0.0.1:3000/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',
+ },
project: {
__typename: 'Project',
id: '1',
@@ -113,6 +122,7 @@ export const workItemQueryResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -152,7 +162,11 @@ export const updateWorkItemMutationResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: '2022-08-08T12:41:54Z',
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -176,6 +190,7 @@ export const updateWorkItemMutationResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -200,6 +215,14 @@ export const updateWorkItemMutationResponse = {
nodes: [mockAssignees[0]],
},
},
+ {
+ __typename: 'WorkItemWidgetLabels',
+ type: 'LABELS',
+ allowsScopedLabels: false,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
],
},
},
@@ -264,7 +287,6 @@ export const workItemResponseFactory = ({
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
datesWidgetPresent = true,
- labelsWidgetPresent = true,
weightWidgetPresent = true,
progressWidgetPresent = true,
milestoneWidgetPresent = true,
@@ -273,12 +295,17 @@ export const workItemResponseFactory = ({
notesWidgetPresent = true,
confidential = false,
canInviteMembers = false,
+ labelsWidgetPresent = true,
+ labels = mockLabels,
allowsScopedLabels = false,
lastEditedAt = null,
lastEditedBy = null,
withCheckboxes = false,
parent = mockParent.parent,
workItemType = taskType,
+ author = mockAssignees[0],
+ createdAt = '2022-08-03T12:41:54Z',
+ updatedAt = '2022-08-08T12:32:54Z',
} = {}) => ({
data: {
workItem: {
@@ -289,8 +316,10 @@ export const workItemResponseFactory = ({
state: 'OPEN',
description: 'description',
confidential,
- createdAt: '2022-08-03T12:41:54Z',
+ createdAt,
+ updatedAt,
closedAt: null,
+ author,
project: {
__typename: 'Project',
id: '1',
@@ -330,7 +359,7 @@ export const workItemResponseFactory = ({
type: 'LABELS',
allowsScopedLabels,
labels: {
- nodes: mockLabels,
+ nodes: labels,
},
}
: { type: 'MOCK TYPE' },
@@ -409,6 +438,7 @@ export const workItemResponseFactory = ({
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '5',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -441,6 +471,28 @@ export const workItemResponseFactory = ({
},
});
+export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ issuable: {
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ __typename: 'Iteration',
+ },
+ milestone: {
+ id: 'gid://gitlab/Milestone/28',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -470,7 +522,11 @@ export const createWorkItemMutationResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -494,6 +550,16 @@ export const createWorkItemMutationResponse = {
},
};
+export const createWorkItemMutationErrorResponse = {
+ data: {
+ workItemCreate: {
+ __typename: 'WorkItemCreatePayload',
+ workItem: null,
+ errors: ['an error'],
+ },
+ },
+};
+
export const createWorkItemFromTaskMutationResponse = {
data: {
workItemCreateFromTask: {
@@ -1045,11 +1111,15 @@ export const workItemObjectiveWithChild = {
deleteWorkItem: true,
updateWorkItem: true,
},
+ author: {
+ ...mockAssignees[0],
+ },
title: 'Objective',
description: 'Objective description',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
widgets: [
{
@@ -1190,7 +1260,11 @@ export const changeWorkItemParentMutationResponse = {
title: 'Foo',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -1557,7 +1631,7 @@ export const projectWorkItemResponse = {
export const mockWorkItemNotesResponse = {
data: {
workItem: {
- id: 'gid://gitlab/WorkItem/600',
+ id: 'gid://gitlab/WorkItem/1',
iid: '60',
widgets: [
{
@@ -1596,20 +1670,30 @@ export const mockWorkItemNotesResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1629,20 +1713,30 @@ export const mockWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723565678',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1662,19 +1756,29 @@ export const mockWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
notes: {
nodes: [
{
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'changed weight to **89**',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1753,20 +1857,31 @@ export const mockWorkItemNotesByIidResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
nodes: [
{
id: 'gid://gitlab/Note/2428',
+ body: 'added 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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1786,21 +1901,32 @@ export const mockWorkItemNotesByIidResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id:
'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1820,21 +1946,33 @@ export const mockWorkItemNotesByIidResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3',
+ id: 'gid://gitlab/Discussion/addbc177f7664699a135130ab05ffb78c57e4db3',
notes: {
nodes: [
{
id:
'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
+ body:
+ 'changed iteration to Et autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1910,20 +2048,30 @@ export const mockMoreWorkItemNotesResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1943,20 +2091,30 @@ export const mockMoreWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1976,19 +2134,29 @@ export const mockMoreWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
notes: {
nodes: [
{
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed weight to **89**',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -2022,6 +2190,55 @@ export const createWorkItemNoteResponse = {
data: {
createNote: {
errors: [],
+ note: {
+ id: 'gid://gitlab/Note/569',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/569',
+ body: 'Main comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Main comment</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-25T04:49:46Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ body: 'Latest 22',
+ bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Latest 22</p>',
+ __typename: 'Note',
+ },
__typename: 'CreateNotePayload',
},
},
@@ -2029,14 +2246,25 @@ export const createWorkItemNoteResponse = {
export const mockWorkItemCommentNote = {
id: 'gid://gitlab/Note/158',
+ body: 'How are you ? what do you think about this ?',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: false,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -2048,3 +2276,174 @@ export const mockWorkItemCommentNote = {
__typename: 'UserCore',
},
};
+
+export const mockWorkItemNotesResponseWithComments = {
+ 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: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/174',
+ body: 'Separate thread',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-12T07:47:40Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/235',
+ body: 'Thread comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-18T09:09:54Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'Main thread 2',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ system: false,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __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',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
new file mode 100644
index 00000000000..aa24b80cf08
--- /dev/null
+++ b/spec/frontend/work_items/utils_spec.js
@@ -0,0 +1,27 @@
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+
+describe('autocompleteDataSources', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(autocompleteDataSources('project/group', '2')).toMatchObject({
+ commands: '/foobar/project/group/-/autocomplete_sources/commands?type=WorkItem&type_id=2',
+ labels: '/foobar/project/group/-/autocomplete_sources/labels?type=WorkItem&type_id=2',
+ members: '/foobar/project/group/-/autocomplete_sources/members?type=WorkItem&type_id=2',
+ });
+ });
+});
+
+describe('markdownPreviewPath', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(markdownPreviewPath('project/group', '2')).toEqual(
+ '/foobar/project/group/preview_markdown?target_type=WorkItem&target_id=2',
+ );
+ });
+});