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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/work_items')
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js96
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js249
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js51
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js17
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js84
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_replying_spec.js8
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js96
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js75
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js11
-rw-r--r--spec/frontend/work_items/components/work_item_award_emoji_spec.js165
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js294
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js41
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js86
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js116
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js12
-rw-r--r--spec/frontend/work_items/graphql/cache_utils_spec.js153
-rw-r--r--spec/frontend/work_items/mock_data.js721
-rw-r--r--spec/frontend/work_items/notes/collapse_utils_spec.js29
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js3
22 files changed, 1680 insertions, 660 deletions
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 fd5f373d076..03f1aa356ad 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -1,54 +1,32 @@
import { GlIcon } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
-import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import MockAdapter from 'axios-mock-adapter';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue';
-import NoteHeader from '~/notes/components/note_header.vue';
+import { workItemSystemNoteWithMetadata } from 'jest/work_items/mock_data';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/behaviors/markdown/render_gfm');
-describe('system note component', () => {
+describe('Work Items system note component', () => {
let wrapper;
- let props;
let mock;
- const findTimelineIcon = () => wrapper.findComponent(GlIcon);
- const findSystemNoteMessage = () => wrapper.findComponent(NoteHeader);
- const findOutdatedLineButton = () =>
- wrapper.findComponent('[data-testid="outdated-lines-change-btn"]');
- const findOutdatedLines = () => wrapper.findComponent('[data-testid="outdated-lines"]');
+ const createComponent = ({ note = workItemSystemNoteWithMetadata } = {}) => {
+ mock = new MockAdapter(axios);
- const createComponent = (propsData = {}) => {
wrapper = shallowMount(WorkItemSystemNote, {
- propsData,
- slots: {
- 'extra-controls':
- '<gl-button data-testid="outdated-lines-change-btn">Compare with last version</gl-button>',
+ propsData: {
+ note,
},
});
};
- beforeEach(() => {
- props = {
- note: {
- id: '1424',
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatarUrl: 'path',
- path: '/root',
- },
- bodyHtml: '<p dir="auto">closed</p>',
- systemNoteIconName: 'status_closed',
- createdAt: '2017-08-02T10:51:58.559Z',
- },
- };
+ const findTimelineIcon = () => wrapper.findComponent(GlIcon);
+ const findComparePreviousVersionButton = () => wrapper.find('[data-testid="compare-btn"]');
+ beforeEach(() => {
+ createComponent();
mock = new MockAdapter(axios);
});
@@ -57,56 +35,16 @@ describe('system note component', () => {
});
it('should render a list item with correct id', () => {
- createComponent(props);
-
- expect(wrapper.attributes('id')).toBe(`note_${props.note.id}`);
- });
-
- // Note: The test case below is to handle a use case related to vuex store but since this does not
- // have a vuex store , disabling it now will be fixing it in the next iteration
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('should render target class is note is target note', () => {
- createComponent(props);
-
- expect(wrapper.classes()).toContain('target');
+ expect(wrapper.attributes('id')).toBe(
+ `note_${getIdFromGraphQLId(workItemSystemNoteWithMetadata.id)}`,
+ );
});
it('should render svg icon', () => {
- createComponent(props);
-
expect(findTimelineIcon().exists()).toBe(true);
});
- // Redcarpet Markdown renderer wraps text in `<p>` tags
- // we need to strip them because they break layout of commit lists in system notes:
- // https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
- it('removes wrapping paragraph from note HTML', () => {
- createComponent(props);
-
- expect(findSystemNoteMessage().html()).toContain('<span>closed</span>');
- });
-
- it('should renderGFM onMount', () => {
- createComponent(props);
-
- expect(renderGFM).toHaveBeenCalled();
- });
-
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('renders outdated code lines', async () => {
- mock
- .onGet('/outdated_line_change_path')
- .reply(HTTP_STATUS_OK, [
- { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
- ]);
-
- createComponent({
- note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
- });
-
- await findOutdatedLineButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findOutdatedLines().exists()).toBe(true);
+ it('should not show compare previous version for FOSS', () => {
+ expect(findComparePreviousVersionButton().exists()).toBe(false);
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 739340f4936..e6d20dcb0d9 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -32,15 +32,18 @@ describe('Work item add note', () => {
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
+ const findWorkItemLockedComponent = () => wrapper.findComponent(WorkItemCommentLocked);
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
canUpdate = true,
+ canCreateNote = true,
workItemIid = '1',
- workItemResponse = workItemByIidResponseFactory({ canUpdate }),
+ workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }),
signedIn = true,
isEditing = true,
workItemType = 'Task',
+ isInternalThread = false,
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
if (signedIn) {
@@ -65,6 +68,7 @@ describe('Work item add note', () => {
workItemType,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
+ isInternalThread,
},
stubs: {
WorkItemCommentLocked,
@@ -79,142 +83,170 @@ describe('Work item add note', () => {
};
describe('adding a comment', () => {
- it('calls update widgets mutation', async () => {
- const noteText = 'updated desc';
-
- await createComponent({
- isEditing: true,
- signedIn: true,
+ describe.each`
+ isInternalComment
+ ${false}
+ ${true}
+ `('when internal comment is $isInternalComment', ({ isInternalComment }) => {
+ it('calls update widgets mutation', async () => {
+ const noteText = 'updated desc';
+
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ });
+
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: noteText,
+ isNoteInternal: isInternalComment,
+ });
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ noteableId: workItemId,
+ body: noteText,
+ discussionId: null,
+ internal: isInternalComment,
+ },
+ });
});
- findCommentForm().vm.$emit('submitForm', noteText);
+ it('tracks adding comment', async () => {
+ await createComponent();
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await waitForPromises();
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: 'test',
+ isNoteInternal: isInternalComment,
+ });
- expect(mutationSuccessHandler).toHaveBeenCalledWith({
- input: {
- noteableId: workItemId,
- body: noteText,
- discussionId: null,
- },
- });
- });
+ await waitForPromises();
- it('tracks adding comment', async () => {
- await createComponent();
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'add_work_item_comment', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_comment',
+ property: 'type_Task',
+ });
+ });
- findCommentForm().vm.$emit('submitForm', 'test');
+ it('emits `replied` event and hides form after successful mutation', async () => {
+ await createComponent({ isEditing: true, signedIn: true });
- await waitForPromises();
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: 'some text',
+ isNoteInternal: isInternalComment,
+ });
+ await waitForPromises();
- expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'add_work_item_comment', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_comment',
- property: 'type_Task',
+ expect(wrapper.emitted('replied')).toEqual([[]]);
});
- });
-
- it('emits `replied` event and hides form after successful mutation', async () => {
- await createComponent({ isEditing: true, signedIn: true });
- findCommentForm().vm.$emit('submitForm', 'some text');
- await waitForPromises();
+ it('clears a draft after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ });
- expect(wrapper.emitted('replied')).toEqual([[]]);
- });
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: 'some text',
+ isNoteInternal: isInternalComment,
+ });
+ await waitForPromises();
- it('clears a draft after successful mutation', async () => {
- await createComponent({
- isEditing: true,
- signedIn: true,
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
});
- 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';
- it('emits error when mutation returns error', async () => {
- const error = 'eror';
-
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockResolvedValue({
- data: {
- createNote: {
- note: {
- id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
- discussion: {
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ createNote: {
+ note: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
- notes: {
- nodes: [],
- __typename: 'NoteConnection',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
},
- __typename: 'Discussion',
+ __typename: 'Note',
},
- __typename: 'Note',
+ __typename: 'CreateNotePayload',
+ errors: [error],
},
- __typename: 'CreateNotePayload',
- errors: [error],
},
- },
- }),
- });
+ }),
+ });
- findCommentForm().vm.$emit('submitForm', 'updated desc');
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: 'updated desc',
+ isNoteInternal: isInternalComment,
+ });
- await waitForPromises();
+ await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- it('emits error when mutation fails', async () => {
- const error = 'eror';
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
- });
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
- findCommentForm().vm.$emit('submitForm', 'updated desc');
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: 'updated desc',
+ isNoteInternal: isInternalComment,
+ });
- await waitForPromises();
+ await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- it('ignores errors when mutation returns additional information as errors for quick actions', async () => {
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockResolvedValue({
- data: {
- createNote: {
- note: {
- id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
- discussion: {
+ it('ignores errors when mutation returns additional information as errors for quick actions', async () => {
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ createNote: {
+ note: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
- notes: {
- nodes: [],
- __typename: 'NoteConnection',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
},
- __typename: 'Discussion',
+ __typename: 'Note',
},
- __typename: 'Note',
+ __typename: 'CreateNotePayload',
+ errors: ['Commands only Removed assignee @foobar.', 'Command names ["unassign"]'],
},
- __typename: 'CreateNotePayload',
- errors: ['Commands only Removed assignee @foobar.', 'Command names ["unassign"]'],
},
- },
- }),
- });
+ }),
+ });
- findCommentForm().vm.$emit('submitForm', 'updated desc');
+ findCommentForm().vm.$emit('submitForm', {
+ commentText: 'updated desc',
+ isNoteInternal: isInternalComment,
+ });
- await waitForPromises();
+ await waitForPromises();
- expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ });
});
});
@@ -225,8 +257,23 @@ describe('Work item add note', () => {
});
it('skips calling the work item query when missing workItemIid', async () => {
- await createComponent({ workItemIid: null, isEditing: false });
+ await createComponent({ workItemIid: '', isEditing: false });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
+
+ it('wrapper adds `internal-note` class when internal thread', async () => {
+ await createComponent({ isInternalThread: true });
+
+ expect(wrapper.attributes('class')).toContain('internal-note');
+ });
+
+ describe('when work item`createNote` permission false', () => {
+ it('cannot add comment', async () => {
+ await createComponent({ isEditing: false, canCreateNote: false });
+
+ expect(findWorkItemLockedComponent().exists()).toBe(true);
+ expect(findCommentForm().exists()).toBe(false);
+ });
+ });
});
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
index 147f2904761..6c00d52aac5 100644
--- 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
@@ -1,6 +1,8 @@
+import { GlFormCheckbox, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { createMockDirective } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import * as autosave from '~/lib/utils/autosave';
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
@@ -40,6 +42,8 @@ describe('Work item comment form component', () => {
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
+ const findInternalNoteCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findInternalNoteTooltipIcon = () => wrapper.findComponent(GlIcon);
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
@@ -68,6 +72,9 @@ describe('Work item comment form component', () => {
provide: {
fullPath: 'test-project-path',
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -168,7 +175,9 @@ describe('Work item comment form component', () => {
createComponent();
findConfirmButton().vm.$emit('click');
- expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ expect(wrapper.emitted('submitForm')).toEqual([
+ [{ commentText: draftComment, isNoteInternal: false }],
+ ]);
});
it('emits `submitForm` event on pressing enter with meta key on markdown editor', () => {
@@ -178,7 +187,9 @@ describe('Work item comment form component', () => {
new KeyboardEvent('keydown', { key: ENTER_KEY, metaKey: true }),
);
- expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ expect(wrapper.emitted('submitForm')).toEqual([
+ [{ commentText: draftComment, isNoteInternal: false }],
+ ]);
});
it('emits `submitForm` event on pressing ctrl+enter on markdown editor', () => {
@@ -188,7 +199,9 @@ describe('Work item comment form component', () => {
new KeyboardEvent('keydown', { key: ENTER_KEY, ctrlKey: true }),
);
- expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ expect(wrapper.emitted('submitForm')).toEqual([
+ [{ commentText: draftComment, isNoteInternal: false }],
+ ]);
});
describe('when used as a top level/is a new discussion', () => {
@@ -249,4 +262,36 @@ describe('Work item comment form component', () => {
});
});
});
+
+ describe('internal note', () => {
+ it('internal note checkbox should not be visible by default', () => {
+ createComponent();
+
+ expect(findInternalNoteCheckbox().exists()).toBe(false);
+ });
+
+ describe('when used as a new discussion', () => {
+ beforeEach(() => {
+ createComponent({ isNewDiscussion: true });
+ });
+
+ it('should have the add as internal note capability', () => {
+ expect(findInternalNoteCheckbox().exists()).toBe(true);
+ });
+
+ it('should have the tooltip explaining the internal note capabilities', () => {
+ expect(findInternalNoteTooltipIcon().exists()).toBe(true);
+ expect(findInternalNoteTooltipIcon().attributes('title')).toBe(
+ WorkItemCommentForm.i18n.internalVisibility,
+ );
+ });
+
+ it('should change the submit button text on change of value', async () => {
+ findInternalNoteCheckbox().vm.$emit('input', true);
+ await nextTick();
+
+ expect(findConfirmButton().text()).toBe(WorkItemCommentForm.i18n.addInternalNote);
+ });
+ });
+ });
});
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
index fac5011b6af..9d22a64f2cb 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -90,6 +90,16 @@ describe('Work Item Discussion', () => {
expect(findWorkItemAddNote().exists()).toBe(true);
expect(findWorkItemAddNote().props('autofocus')).toBe(true);
});
+
+ it('should send the correct props is when the main comment is internal', async () => {
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ expect(findWorkItemAddNote().props('isInternalThread')).toBe(
+ mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes[0].internal,
+ );
+ });
});
describe('When replying to any comment', () => {
@@ -115,6 +125,13 @@ describe('Work Item Discussion', () => {
expect(findToggleRepliesWidget().exists()).toBe(true);
expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
});
+
+ it('should pass `is-internal-note` props to make sure the correct background is set', () => {
+ expect(findWorkItemNoteReplying().exists()).toBe(true);
+ expect(findWorkItemNoteReplying().props('isInternalNote')).toBe(
+ mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes[0].internal,
+ );
+ });
});
it('emits `deleteNote` event with correct parameter when child note component emits `deleteNote` event', () => {
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
index 99bf391e261..2e901783e07 100644
--- 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
@@ -1,8 +1,9 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } 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 { createMockDirective } from 'helpers/vue_mock_directive';
import EmojiPicker from '~/emoji/components/picker.vue';
import waitForPromises from 'helpers/wait_for_promises';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
@@ -18,11 +19,14 @@ describe('Work Item Note Actions', () => {
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]');
+ const findAuthorBadge = () => wrapper.find('[data-testid="author-badge"]');
+ const findMaxAccessLevelBadge = () => wrapper.find('[data-testid="max-access-level-badge"]');
+ const findContributorBadge = () => wrapper.find('[data-testid="contributor-badge"]');
const addEmojiMutationResolver = jest.fn().mockResolvedValue({
data: {
@@ -41,6 +45,11 @@ describe('Work Item Note Actions', () => {
showAwardEmoji = true,
showAssignUnassign = false,
canReportAbuse = false,
+ workItemType = 'Task',
+ isWorkItemAuthor = false,
+ isAuthorContributor = false,
+ maxAccessLevelOfAuthor = '',
+ projectName = 'Project name',
} = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
@@ -50,6 +59,11 @@ describe('Work Item Note Actions', () => {
showAwardEmoji,
showAssignUnassign,
canReportAbuse,
+ workItemType,
+ isWorkItemAuthor,
+ isAuthorContributor,
+ maxAccessLevelOfAuthor,
+ projectName,
},
provide: {
glFeatures: {
@@ -60,7 +74,11 @@ describe('Work Item Note Actions', () => {
EmojiPicker: EmojiPickerStub,
},
apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]),
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
+ wrapper.vm.$refs.dropdown.close = jest.fn();
};
describe('reply button', () => {
@@ -152,7 +170,7 @@ describe('Work Item Note Actions', () => {
showEdit: true,
});
- findDeleteNoteButton().vm.$emit('click');
+ findDeleteNoteButton().vm.$emit('action');
expect(wrapper.emitted('deleteNote')).toEqual([[]]);
});
@@ -167,7 +185,7 @@ describe('Work Item Note Actions', () => {
});
it('should emit `notifyCopyDone` event when copy link note action is clicked', () => {
- findCopyLinkButton().vm.$emit('click');
+ findCopyLinkButton().vm.$emit('action');
expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]);
});
@@ -193,7 +211,7 @@ describe('Work Item Note Actions', () => {
showAssignUnassign: true,
});
- findAssignUnassignButton().vm.$emit('click');
+ findAssignUnassignButton().vm.$emit('action');
expect(wrapper.emitted('assignUser')).toEqual([[]]);
});
@@ -219,9 +237,63 @@ describe('Work Item Note Actions', () => {
canReportAbuse: true,
});
- findReportAbuseToAdminButton().vm.$emit('click');
+ findReportAbuseToAdminButton().vm.$emit('action');
expect(wrapper.emitted('reportAbuse')).toEqual([[]]);
});
});
+
+ describe('user role badges', () => {
+ describe('author badge', () => {
+ it('does not show the author badge by default', () => {
+ createComponent();
+
+ expect(findAuthorBadge().exists()).toBe(false);
+ });
+
+ it('shows the author badge when the work item is author by the current User', () => {
+ createComponent({ isWorkItemAuthor: true });
+
+ expect(findAuthorBadge().exists()).toBe(true);
+ expect(findAuthorBadge().text()).toBe('Author');
+ expect(findAuthorBadge().attributes('title')).toBe('This user is the author of this task.');
+ });
+ });
+
+ describe('Max access level badge', () => {
+ it('does not show the access level badge by default', () => {
+ createComponent();
+
+ expect(findMaxAccessLevelBadge().exists()).toBe(false);
+ });
+
+ it('shows the access badge when we have a valid value', () => {
+ createComponent({ maxAccessLevelOfAuthor: 'Owner' });
+
+ expect(findMaxAccessLevelBadge().exists()).toBe(true);
+ expect(findMaxAccessLevelBadge().text()).toBe('Owner');
+ expect(findMaxAccessLevelBadge().attributes('title')).toBe(
+ 'This user has the owner role in the Project name project.',
+ );
+ });
+ });
+
+ describe('Contributor badge', () => {
+ it('does not show the contributor badge by default', () => {
+ createComponent();
+
+ expect(findContributorBadge().exists()).toBe(false);
+ });
+
+ it('shows the contributor badge the note author is a contributor', () => {
+ createComponent({ isAuthorContributor: true });
+
+ expect(findContributorBadge().exists()).toBe(true);
+ expect(findContributorBadge().text()).toBe('Contributor');
+ expect(findContributorBadge().attributes('title')).toBe(
+ 'This user has previously committed to the Project name project.',
+ );
+ });
+ });
+ });
});
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
index 225cc3bacaf..5a6894400b6 100644
--- 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
@@ -10,10 +10,11 @@ describe('Work Item Note Replying', () => {
const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
- const createComponent = ({ body = mockNoteBody } = {}) => {
+ const createComponent = ({ body = mockNoteBody, isInternalNote = false } = {}) => {
wrapper = shallowMount(WorkItemNoteReplying, {
propsData: {
body,
+ isInternalNote,
},
});
@@ -31,4 +32,9 @@ describe('Work Item Note Replying', () => {
expect(findTimelineEntry().exists()).toBe(true);
expect(findNoteHeader().html()).toMatchSnapshot();
});
+
+ it('should have the correct class when internal note', () => {
+ createComponent({ isInternalNote: true });
+ expect(findTimelineEntry().classes()).toContain('internal-note');
+ });
});
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 f2cf5171cc1..8dbd2818fc5 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
@@ -20,6 +20,8 @@ import {
updateWorkItemMutationResponse,
workItemByIidResponseFactory,
workItemQueryResponse,
+ mockWorkItemCommentNoteByContributor,
+ mockWorkItemCommentByMaintainer,
} from 'jest/work_items/mock_data';
import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { mockTracking } from 'helpers/tracking_helper';
@@ -33,6 +35,23 @@ describe('Work Item Note', () => {
const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+ const mockWorkItemByDifferentUser = {
+ data: {
+ workItem: {
+ ...workItemQueryResponse.data.workItem,
+ author: {
+ avatarUrl:
+ 'http://127.0.0.1:3000/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/2',
+ name: 'User 1',
+ username: 'user1',
+ webUrl: 'http://127.0.0.1:3000/user1',
+ __typename: 'UserCore',
+ },
+ },
+ },
+ };
+
const successHandler = jest.fn().mockResolvedValue({
data: {
updateNote: {
@@ -47,6 +66,9 @@ describe('Work Item Note', () => {
});
const workItemResponseHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+ const workItemByAuthoredByDifferentUser = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemByDifferentUser);
const updateWorkItemMutationSuccessHandler = jest
.fn()
@@ -69,6 +91,7 @@ describe('Work Item Note', () => {
workItemId = mockWorkItemId,
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
assignees = mockAssignees,
+ workItemByIidResponseHandler = workItemResponseHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
provide: {
@@ -85,7 +108,7 @@ describe('Work Item Note', () => {
assignees,
},
apolloProvider: mockApollo([
- [workItemByIidQuery, workItemResponseHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
[updateWorkItemNoteMutation, updateNoteMutationHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
@@ -133,7 +156,7 @@ describe('Work Item Note', () => {
findNoteActions().vm.$emit('startEditing');
await nextTick();
- findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ findCommentForm().vm.$emit('submitForm', { commentText: updatedNoteText });
expect(successHandler).toHaveBeenCalledWith({
input: {
@@ -148,7 +171,7 @@ describe('Work Item Note', () => {
findNoteActions().vm.$emit('startEditing');
await nextTick();
- findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ findCommentForm().vm.$emit('submitForm', { commentText: updatedNoteText });
await waitForPromises();
expect(findCommentForm().exists()).toBe(false);
@@ -161,7 +184,7 @@ describe('Work Item Note', () => {
findNoteActions().vm.$emit('startEditing');
await nextTick();
- findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ findCommentForm().vm.$emit('submitForm', { commentText: updatedNoteText });
await waitForPromises();
});
@@ -215,8 +238,9 @@ describe('Work Item Note', () => {
});
describe('main comment', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent({ isFirstNote: true });
+ await waitForPromises();
});
it('should have the note header, actions and body', () => {
@@ -229,6 +253,10 @@ describe('Work Item Note', () => {
it('should have the reply button props', () => {
expect(findNoteActions().props('showReply')).toBe(true);
});
+
+ it('should have the project name', () => {
+ expect(findNoteActions().props('projectName')).toBe('Project name');
+ });
});
describe('comment threads', () => {
@@ -318,5 +346,63 @@ describe('Work Item Note', () => {
},
);
});
+
+ describe('internal note', () => {
+ it('does not have the internal note class set by default', () => {
+ createComponent();
+ expect(findTimelineEntryItem().classes()).not.toContain('internal-note');
+ });
+
+ it('timeline entry item and note header has the class for internal notes', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ internal: true,
+ },
+ });
+ expect(findTimelineEntryItem().classes()).toContain('internal-note');
+ expect(findNoteHeader().props('isInternalNote')).toBe(true);
+ });
+ });
+
+ describe('author and user role badges', () => {
+ describe('author badge props', () => {
+ it.each`
+ isWorkItemAuthor | sameAsCurrentUser | workItemByIidResponseHandler
+ ${true} | ${'same as'} | ${workItemResponseHandler}
+ ${false} | ${'not same as'} | ${workItemByAuthoredByDifferentUser}
+ `(
+ 'should pass correct isWorkItemAuthor `$isWorkItemAuthor` to note actions when author is $sameAsCurrentUser as current note',
+ async ({ isWorkItemAuthor, workItemByIidResponseHandler }) => {
+ createComponent({ workItemByIidResponseHandler });
+ await waitForPromises();
+
+ expect(findNoteActions().props('isWorkItemAuthor')).toBe(isWorkItemAuthor);
+ },
+ );
+ });
+
+ describe('Max access level badge', () => {
+ it('should pass the max access badge props', async () => {
+ createComponent({ note: mockWorkItemCommentByMaintainer });
+ await waitForPromises();
+
+ expect(findNoteActions().props('maxAccessLevelOfAuthor')).toBe(
+ mockWorkItemCommentByMaintainer.maxAccessLevelOfAuthor,
+ );
+ });
+ });
+
+ describe('Contributor badge', () => {
+ it('should pass the contributor props', async () => {
+ createComponent({ note: mockWorkItemCommentNoteByContributor });
+ await waitForPromises();
+
+ expect(findNoteActions().props('isAuthorContributor')).toBe(
+ mockWorkItemCommentNoteByContributor.authorIsContributor,
+ );
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 0045abe50d0..e03c6a7e28d 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,9 +1,12 @@
import { GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
import Vue 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 { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import { isLoggedIn } from '~/lib/utils/common_utils';
import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
@@ -13,6 +16,8 @@ import {
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
+ TEST_ID_COPY_REFERENCE_ACTION,
+ TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
} from '~/work_items/constants';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
@@ -31,8 +36,10 @@ describe('WorkItemActions component', () => {
Vue.use(VueApollo);
let wrapper;
- let glModalDirective;
let mockApollo;
+ const mockWorkItemReference = 'gitlab-org/gitlab-test#1';
+ const mockWorkItemCreateNoteEmail =
+ 'gitlab-incoming+gitlab-org-gitlab-test-2-ddpzuq0zd2wefzofcpcdr3dg7-issue-1@gmail.com';
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
@@ -41,6 +48,9 @@ describe('WorkItemActions component', () => {
wrapper.findByTestId(TEST_ID_NOTIFICATIONS_TOGGLE_ACTION);
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
const findPromoteButton = () => wrapper.findByTestId(TEST_ID_PROMOTE_ACTION);
+ const findCopyReferenceButton = () => wrapper.findByTestId(TEST_ID_COPY_REFERENCE_ACTION);
+ const findCopyCreateNoteEmailButton = () =>
+ wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
const findDropdownItemsActual = () =>
findDropdownItems().wrappers.map((x) => {
@@ -55,6 +65,7 @@ describe('WorkItemActions component', () => {
});
const findNotificationsToggle = () => wrapper.findComponent(GlToggle);
+ const modalShowSpy = jest.fn();
const $toast = {
show: jest.fn(),
hide: jest.fn(),
@@ -77,9 +88,10 @@ describe('WorkItemActions component', () => {
notificationsMock = [updateWorkItemNotificationsMutation, jest.fn()],
convertWorkItemMutationHandler = convertWorkItemMutationSuccessHandler,
workItemType = 'Task',
+ workItemReference = mockWorkItemReference,
+ workItemCreateNoteEmail = mockWorkItemCreateNoteEmail,
} = {}) => {
const handlers = [notificationsMock];
- glModalDirective = jest.fn();
mockApollo = createMockApollo([
...handlers,
[convertWorkItemMutation, convertWorkItemMutationHandler],
@@ -96,13 +108,8 @@ describe('WorkItemActions component', () => {
subscribed,
isParentConfidential,
workItemType,
- },
- directives: {
- glModal: {
- bind(_, { value }) {
- glModalDirective(value);
- },
- },
+ workItemReference,
+ workItemCreateNoteEmail,
},
provide: {
fullPath: 'gitlab-org/gitlab',
@@ -111,6 +118,13 @@ describe('WorkItemActions component', () => {
mocks: {
$toast,
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: modalShowSpy,
+ },
+ }),
+ },
});
};
@@ -141,6 +155,14 @@ describe('WorkItemActions component', () => {
text: 'Turn on confidentiality',
},
{
+ testId: TEST_ID_COPY_REFERENCE_ACTION,
+ text: 'Copy reference',
+ },
+ {
+ testId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
+ text: 'Copy task email address',
+ },
+ {
divider: true,
},
{
@@ -189,7 +211,7 @@ describe('WorkItemActions component', () => {
findDeleteButton().vm.$emit('click');
- expect(glModalDirective).toHaveBeenCalled();
+ expect(modalShowSpy).toHaveBeenCalled();
});
it('emits event when clicking OK button', () => {
@@ -359,4 +381,37 @@ describe('WorkItemActions component', () => {
]);
});
});
+
+ describe('copy reference action', () => {
+ it('shows toast when user clicks on the action', () => {
+ createComponent();
+
+ expect(findCopyReferenceButton().exists()).toBe(true);
+ findCopyReferenceButton().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Reference copied');
+ });
+ });
+
+ describe('copy email address action', () => {
+ it.each(['key result', 'objective'])(
+ 'renders correct button name when work item is %s',
+ (workItemType) => {
+ createComponent({ workItemType });
+
+ expect(findCopyCreateNoteEmailButton().text()).toEqual(
+ `Copy ${workItemType} email address`,
+ );
+ },
+ );
+
+ it('shows toast when user clicks on the action', () => {
+ createComponent();
+
+ expect(findCopyCreateNoteEmailButton().exists()).toBe(true);
+ findCopyCreateNoteEmailButton().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Email address copied');
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 25b0b74c217..94d47bfb3be 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -26,6 +26,7 @@ import {
updateWorkItemMutationResponse,
projectMembersResponseWithCurrentUserWithNextPage,
projectMembersResponseWithNoMatchingUsers,
+ projectMembersResponseWithDuplicates,
} from '../mock_data';
Vue.use(VueApollo);
@@ -529,4 +530,14 @@ describe('WorkItemAssignees component', () => {
});
});
});
+
+ it('filters out the users with the same ID from the list of project members', async () => {
+ createComponent({
+ searchQueryHandler: jest.fn().mockResolvedValue(projectMembersResponseWithDuplicates),
+ });
+ findTokenSelector().vm.$emit('focus');
+ await waitForPromises();
+
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
index f87c0e3f357..82be6d990e4 100644
--- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -8,19 +8,15 @@ import waitForPromises from 'helpers/wait_for_promises';
import { isLoggedIn } from '~/lib/utils/common_utils';
import AwardList from '~/vue_shared/components/awards_list.vue';
import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vue';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
-import {
- EMOJI_ACTION_REMOVE,
- EMOJI_ACTION_ADD,
- EMOJI_THUMBSUP,
- EMOJI_THUMBSDOWN,
-} from '~/work_items/constants';
+import updateAwardEmojiMutation from '~/work_items/graphql/update_award_emoji.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
import {
workItemByIidResponseFactory,
mockAwardsWidget,
- updateWorkItemMutationResponseFactory,
mockAwardEmojiThumbsUp,
+ getAwardEmojiResponse,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
@@ -28,43 +24,61 @@ Vue.use(VueApollo);
describe('WorkItemAwardEmoji component', () => {
let wrapper;
+ let mockApolloProvider;
const errorMessage = 'Failed to update the award';
-
const workItemQueryResponse = workItemByIidResponseFactory();
- const workItemSuccessHandler = jest
- .fn()
- .mockResolvedValue(updateWorkItemMutationResponseFactory());
- const awardEmojiAddSuccessHandler = jest.fn().mockResolvedValue(
- updateWorkItemMutationResponseFactory({
- awardEmoji: {
- ...mockAwardsWidget,
- nodes: [mockAwardEmojiThumbsUp],
- },
- }),
- );
- const awardEmojiRemoveSuccessHandler = jest.fn().mockResolvedValue(
- updateWorkItemMutationResponseFactory({
- awardEmoji: {
- ...mockAwardsWidget,
- nodes: [],
- },
- }),
- );
- const workItemUpdateFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
+ const workItemQueryAddAwardEmojiResponse = workItemByIidResponseFactory({
+ awardEmoji: { ...mockAwardsWidget, nodes: [mockAwardEmojiThumbsUp] },
+ });
+ const workItemQueryRemoveAwardEmojiResponse = workItemByIidResponseFactory({
+ awardEmoji: { ...mockAwardsWidget, nodes: [] },
+ });
+ const awardEmojiAddSuccessHandler = jest.fn().mockResolvedValue(getAwardEmojiResponse(true));
+ const awardEmojiRemoveSuccessHandler = jest.fn().mockResolvedValue(getAwardEmojiResponse(false));
+ const awardEmojiUpdateFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
+ const mockAwardEmojiDifferentUserThumbsUp = {
+ name: 'thumbsup',
+ __typename: 'AwardEmoji',
+ user: {
+ id: 'gid://gitlab/User/1',
+ name: 'John Doe',
+ __typename: 'UserCore',
+ },
+ };
const createComponent = ({
- mockWorkItemUpdateMutationHandler = [updateWorkItemMutation, workItemSuccessHandler],
+ awardMutationHandler = awardEmojiAddSuccessHandler,
workItem = mockWorkItem,
+ workItemIid = '1',
awardEmoji = { ...mockAwardsWidget, nodes: [] },
} = {}) => {
+ mockApolloProvider = createMockApollo([[updateAwardEmojiMutation, awardMutationHandler]]);
+
+ mockApolloProvider.clients.defaultClient.writeQuery({
+ query: workItemByIidQuery,
+ variables: { fullPath: workItem.project.fullPath, iid: workItemIid },
+ data: {
+ ...workItemQueryResponse.data,
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItem],
+ },
+ },
+ },
+ });
+
wrapper = shallowMount(WorkItemAwardEmoji, {
isLoggedIn: isLoggedIn(),
- apolloProvider: createMockApollo([mockWorkItemUpdateMutationHandler]),
+ apolloProvider: mockApolloProvider,
propsData: {
- workItem,
+ workItemId: workItem.id,
+ workItemFullpath: workItem.project.fullPath,
awardEmoji,
+ workItemIid,
},
});
};
@@ -74,7 +88,8 @@ describe('WorkItemAwardEmoji component', () => {
beforeEach(() => {
isLoggedIn.mockReturnValue(true);
window.gon = {
- current_user_id: 1,
+ current_user_id: 5,
+ current_user_fullname: 'Dave Smith',
};
createComponent();
@@ -85,7 +100,7 @@ describe('WorkItemAwardEmoji component', () => {
expect(findAwardsList().props()).toEqual({
boundary: '',
canAwardEmoji: true,
- currentUserId: 1,
+ currentUserId: 5,
defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
selectedClass: 'selected',
awards: [],
@@ -97,48 +112,70 @@ describe('WorkItemAwardEmoji component', () => {
expect(findAwardsList().props('awards')).toEqual([
{
- id: 1,
name: EMOJI_THUMBSUP,
user: {
id: 5,
+ name: 'Dave Smith',
},
},
{
- id: 2,
name: EMOJI_THUMBSDOWN,
user: {
id: 5,
+ name: 'Dave Smith',
+ },
+ },
+ ]);
+ });
+
+ it('renders awards list given by multiple users', () => {
+ createComponent({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUserThumbsUp],
+ },
+ });
+
+ expect(findAwardsList().props('awards')).toEqual([
+ {
+ name: EMOJI_THUMBSUP,
+ user: {
+ id: 5,
+ name: 'Dave Smith',
+ },
+ },
+ {
+ name: EMOJI_THUMBSUP,
+ user: {
+ id: 1,
+ name: 'John Doe',
},
},
]);
});
it.each`
- expectedAssertion | action | successHandler | mockAwardEmojiNodes
- ${'added'} | ${EMOJI_ACTION_ADD} | ${awardEmojiAddSuccessHandler} | ${[]}
- ${'removed'} | ${EMOJI_ACTION_REMOVE} | ${awardEmojiRemoveSuccessHandler} | ${[mockAwardEmojiThumbsUp]}
+ expectedAssertion | awardEmojiMutationHandler | mockAwardEmojiNodes | workItem
+ ${'added'} | ${awardEmojiAddSuccessHandler} | ${[]} | ${workItemQueryRemoveAwardEmojiResponse.data.workspace.workItems.nodes[0]}
+ ${'removed'} | ${awardEmojiRemoveSuccessHandler} | ${[mockAwardEmojiThumbsUp]} | ${workItemQueryAddAwardEmojiResponse.data.workspace.workItems.nodes[0]}
`(
'calls mutation when an award emoji is $expectedAssertion',
- async ({ action, successHandler, mockAwardEmojiNodes }) => {
+ ({ awardEmojiMutationHandler, mockAwardEmojiNodes, workItem }) => {
createComponent({
- mockWorkItemUpdateMutationHandler: [updateWorkItemMutation, successHandler],
+ awardMutationHandler: awardEmojiMutationHandler,
awardEmoji: {
...mockAwardsWidget,
nodes: mockAwardEmojiNodes,
},
+ workItem,
});
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
- await waitForPromises();
-
- expect(successHandler).toHaveBeenCalledWith({
+ expect(awardEmojiMutationHandler).toHaveBeenCalledWith({
input: {
- id: mockWorkItem.id,
- awardEmojiWidget: {
- action,
- name: EMOJI_THUMBSUP,
- },
+ awardableId: mockWorkItem.id,
+ name: EMOJI_THUMBSUP,
},
});
},
@@ -146,7 +183,7 @@ describe('WorkItemAwardEmoji component', () => {
it('emits error when the update mutation fails', async () => {
createComponent({
- mockWorkItemUpdateMutationHandler: [updateWorkItemMutation, workItemUpdateFailureHandler],
+ awardMutationHandler: awardEmojiUpdateFailureHandler,
});
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
@@ -167,4 +204,32 @@ describe('WorkItemAwardEmoji component', () => {
expect(findAwardsList().props('canAwardEmoji')).toBe(false);
});
});
+
+ describe('when a different users awards same emoji', () => {
+ beforeEach(() => {
+ window.gon = {
+ current_user_id: 1,
+ current_user_fullname: 'John Doe',
+ };
+ });
+
+ it('calls mutation succesfully and adds the award emoji with proper user details', () => {
+ createComponent({
+ awardMutationHandler: awardEmojiAddSuccessHandler,
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp],
+ },
+ });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ expect(awardEmojiAddSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ awardableId: mockWorkItem.id,
+ name: EMOJI_THUMBSUP,
+ },
+ });
+ });
+ });
});
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 62cbb1bacb6..b910e9854f8 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -1,3 +1,4 @@
+import { GlForm } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -7,7 +8,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import EditedAt from '~/issues/show/components/edited.vue';
import { updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
@@ -36,22 +36,18 @@ describe('WorkItemDescription', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
let workItemResponseHandler;
- let workItemsMvc;
- const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findForm = () => wrapper.findComponent(GlForm);
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
const findRenderedDescription = () => wrapper.findComponent(WorkItemDescriptionRendered);
const findEditedAt = () => wrapper.findComponent(EditedAt);
- const editDescription = (newText) => {
- if (workItemsMvc) {
- return findMarkdownEditor().vm.$emit('input', newText);
- }
- return wrapper.find('textarea').setValue(newText);
- };
+ const editDescription = (newText) => findMarkdownEditor().vm.$emit('input', newText);
- const clickCancel = () => wrapper.find('[data-testid="cancel"]').vm.$emit('click');
- const clickSave = () => wrapper.find('[data-testid="save-description"]').vm.$emit('click', {});
+ const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
+ const findSubmitButton = () => wrapper.find('[data-testid="save-description"]');
+ const clickCancel = () => findForm().vm.$emit('reset', new Event('reset'));
+ const clickSave = () => findForm().vm.$emit('submit', new Event('submit'));
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
@@ -75,12 +71,6 @@ describe('WorkItemDescription', () => {
},
provide: {
fullPath: 'test-project-path',
- glFeatures: {
- workItemsMvc,
- },
- },
- stubs: {
- MarkdownField,
},
});
@@ -93,11 +83,15 @@ describe('WorkItemDescription', () => {
}
};
- describe('editing description with workItemsMvc FF enabled', () => {
- beforeEach(() => {
- workItemsMvc = true;
+ it('has a subscription', async () => {
+ await createComponent();
+
+ expect(subscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
});
+ });
+ describe('editing description', () => {
it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
const {
iid,
@@ -113,196 +107,162 @@ describe('WorkItemDescription', () => {
autocompleteDataSources: autocompleteDataSources(fullPath, iid),
});
});
- });
-
- 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 });
+ it('shows edited by text', async () => {
+ const lastEditedAt = '2022-09-21T06:18:42Z';
+ const lastEditedBy = {
+ name: 'Administrator',
+ webPath: '/root',
+ };
+
+ await createComponent({
+ workItemResponse: workItemByIidResponseFactory({ lastEditedAt, lastEditedBy }),
+ });
- expect(findMarkdownField().props()).toMatchObject({
- autocompleteDataSources: autocompleteDataSources(fullPath, iid),
- markdownPreviewPath: markdownPreviewPath(fullPath, iid),
- quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ expect(findEditedAt().props()).toMatchObject({
+ updatedAt: lastEditedAt,
+ updatedByName: lastEditedBy.name,
+ updatedByPath: lastEditedBy.webPath,
});
});
- });
- describe.each([true, false])(
- 'editing description with workItemsMvc %workItemsMvcEnabled',
- (workItemsMvcEnabled) => {
- beforeEach(() => {
- beforeEach(() => {
- workItemsMvc = workItemsMvcEnabled;
- });
- });
+ it('does not show edited by text', async () => {
+ await createComponent();
- it('has a subscription', async () => {
- await createComponent();
+ expect(findEditedAt().exists()).toBe(false);
+ });
- expect(subscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
+ it('cancels when clicking cancel', async () => {
+ await createComponent({
+ isEditing: true,
});
- describe('editing description', () => {
- it('shows edited by text', async () => {
- const lastEditedAt = '2022-09-21T06:18:42Z';
- const lastEditedBy = {
- name: 'Administrator',
- webPath: '/root',
- };
+ clickCancel();
- await createComponent({
- workItemResponse: workItemByIidResponseFactory({ lastEditedAt, lastEditedBy }),
- });
+ await nextTick();
- expect(findEditedAt().props()).toMatchObject({
- updatedAt: lastEditedAt,
- updatedByName: lastEditedBy.name,
- updatedByPath: lastEditedBy.webPath,
- });
- });
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(findMarkdownEditor().exists()).toBe(false);
+ });
- it('does not show edited by text', async () => {
- await createComponent();
+ it('prompts for confirmation when clicking cancel after changes', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- expect(findEditedAt().exists()).toBe(false);
- });
+ editDescription('updated desc');
- it('cancels when clicking cancel', async () => {
- await createComponent({
- isEditing: true,
- });
+ clickCancel();
- clickCancel();
+ await nextTick();
- await nextTick();
+ expect(confirmAction).toHaveBeenCalled();
+ });
- expect(confirmAction).not.toHaveBeenCalled();
- expect(findMarkdownField().exists()).toBe(false);
- });
+ it('calls update widgets mutation', async () => {
+ const updatedDesc = 'updated desc';
- it('prompts for confirmation when clicking cancel after changes', async () => {
- await createComponent({
- isEditing: true,
- });
+ await createComponent({
+ isEditing: true,
+ });
- editDescription('updated desc');
+ editDescription(updatedDesc);
- clickCancel();
+ clickSave();
- await nextTick();
+ await waitForPromises();
- expect(confirmAction).toHaveBeenCalled();
- });
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ descriptionWidget: {
+ description: updatedDesc,
+ },
+ },
+ });
+ });
- it('calls update widgets mutation', async () => {
- const updatedDesc = 'updated desc';
+ it('tracks editing description', async () => {
+ await createComponent({
+ isEditing: true,
+ markdownPreviewPath: '/preview',
+ });
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await createComponent({
- isEditing: true,
- });
+ clickSave();
- editDescription(updatedDesc);
+ await waitForPromises();
- clickSave();
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_description',
+ property: 'type_Task',
+ });
+ });
- await waitForPromises();
+ it('emits error when mutation returns error', async () => {
+ const error = 'eror';
- expect(mutationSuccessHandler).toHaveBeenCalledWith({
- input: {
- id: workItemId,
- descriptionWidget: {
- description: updatedDesc,
- },
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: {},
+ errors: [error],
},
- });
- });
-
- it('tracks editing description', async () => {
- await createComponent({
- isEditing: true,
- markdownPreviewPath: '/preview',
- });
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- clickSave();
-
- await waitForPromises();
-
- expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_description',
- property: 'type_Task',
- });
- });
-
- it('emits error when mutation returns error', async () => {
- const error = 'eror';
+ },
+ }),
+ });
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockResolvedValue({
- data: {
- workItemUpdate: {
- workItem: {},
- errors: [error],
- },
- },
- }),
- });
+ editDescription('updated desc');
- editDescription('updated desc');
+ clickSave();
- clickSave();
+ await waitForPromises();
- await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
- it('emits error when mutation fails', async () => {
- const error = 'eror';
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
- });
+ editDescription('updated desc');
- editDescription('updated desc');
+ clickSave();
- clickSave();
+ await waitForPromises();
- await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ it('autosaves description', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- it('autosaves description', async () => {
- await createComponent({
- isEditing: true,
- });
+ editDescription('updated desc');
- editDescription('updated desc');
+ expect(updateDraft).toHaveBeenCalled();
+ });
- expect(updateDraft).toHaveBeenCalled();
- });
+ it('maps submit and cancel buttons to form actions', async () => {
+ await createComponent({
+ isEditing: true,
});
- it('calls the work item query', async () => {
- await createComponent();
+ expect(findCancelButton().attributes('type')).toBe('reset');
+ expect(findSubmitButton().attributes('type')).toBe('submit');
+ });
+ });
+
+ it('calls the work item query', async () => {
+ await createComponent();
- expect(workItemResponseHandler).toHaveBeenCalled();
- });
- },
- );
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ });
});
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 e305cc310bd..6fa3a70c3eb 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
@@ -33,7 +33,6 @@ describe('WorkItemDetailModal component', () => {
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
const createComponent = ({
- error = false,
deleteWorkItemMutationHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
} = {}) => {
const apolloProvider = createMockApollo([
@@ -46,19 +45,12 @@ describe('WorkItemDetailModal component', () => {
workItemId,
workItemIid: '1',
},
- data() {
- return {
- error,
- };
- },
provide: {
fullPath: 'group/project',
},
stubs: {
GlModal,
- WorkItemDetail: stubComponent(WorkItemDetail, {
- apollo: {},
- }),
+ WorkItemDetail: stubComponent(WorkItemDetail),
},
});
};
@@ -68,14 +60,18 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
- workItemId,
workItemIid: '1',
workItemParentId: null,
});
});
- it('renders alert if there is an error', () => {
- createComponent({ error: true });
+ it('renders alert if there is an error', async () => {
+ createComponent({
+ deleteWorkItemMutationHandler: jest.fn().mockRejectedValue({ message: 'message' }),
+ });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
expect(findAlert().exists()).toBe(true);
});
@@ -87,7 +83,13 @@ describe('WorkItemDetailModal component', () => {
});
it('dismisses the alert on `dismiss` emitted event', async () => {
- createComponent({ error: true });
+ createComponent({
+ deleteWorkItemMutationHandler: jest.fn().mockRejectedValue({ message: 'message' }),
+ });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
findAlert().vm.$emit('dismiss');
await nextTick();
@@ -103,24 +105,19 @@ describe('WorkItemDetailModal component', () => {
it('hides the modal when WorkItemDetail emits `close` event', () => {
createComponent();
- const closeSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
findWorkItemDetail().vm.$emit('close');
- expect(closeSpy).toHaveBeenCalled();
+ expect(hideModal).toHaveBeenCalled();
});
it('updates the work item when WorkItemDetail emits `update-modal` event', async () => {
createComponent();
- findWorkItemDetail().vm.$emit('update-modal', undefined, {
- id: 'updatedId',
- iid: 'updatedIid',
- });
- await waitForPromises();
+ findWorkItemDetail().vm.$emit('update-modal', undefined, { iid: 'updatedIid' });
+ await nextTick();
- expect(findWorkItemDetail().props().workItemId).toEqual('updatedId');
- expect(findWorkItemDetail().props().workItemIid).toEqual('updatedIid');
+ expect(findWorkItemDetail().props('workItemIid')).toBe('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 557ae07969e..d8ba8ea74f2 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -100,7 +100,6 @@ describe('WorkItemDetail component', () => {
const createComponent = ({
isModal = false,
updateInProgress = false,
- workItemId = id,
workItemIid = '1',
handler = successHandler,
subscriptionHandler = titleSubscriptionHandler,
@@ -120,7 +119,10 @@ describe('WorkItemDetail component', () => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
isLoggedIn: isLoggedIn(),
- propsData: { isModal, workItemId, workItemIid },
+ propsData: {
+ isModal,
+ workItemIid,
+ },
data() {
return {
updateInProgress,
@@ -160,9 +162,9 @@ describe('WorkItemDetail component', () => {
setWindowLocation('');
});
- describe('when there is no `workItemId` and no `workItemIid` prop', () => {
+ describe('when there is no `workItemIid` prop', () => {
beforeEach(() => {
- createComponent({ workItemId: null, workItemIid: null });
+ createComponent({ workItemIid: null });
});
it('skips the work item query', () => {
@@ -437,7 +439,7 @@ describe('WorkItemDetail component', () => {
});
it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
- expect(findParentButton().attributes().href).toBe('../../issues/5');
+ expect(findParentButton().attributes().href).toBe('../../-/issues/5');
});
it('sets the parent breadcrumb URL based on parent webUrl when parent type is not `Issue`', async () => {
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
index b4811db8bed..5e8c34d90ee 100644
--- a/spec/frontend/work_items/components/work_item_due_date_spec.js
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
+import { stubComponent } from 'helpers/stub_component';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
@@ -33,6 +34,7 @@ describe('WorkItemDueDate component', () => {
dueDate = null,
startDate = null,
mutationHandler = updateWorkItemMutationHandler,
+ stubs = {},
} = {}) => {
wrapper = mountExtended(WorkItemDueDate, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
@@ -43,6 +45,7 @@ describe('WorkItemDueDate component', () => {
workItemId,
workItemType: 'Task',
},
+ stubs,
});
};
@@ -132,11 +135,21 @@ describe('WorkItemDueDate component', () => {
describe('when the start date is later than the due date', () => {
const startDate = new Date('2030-01-01T00:00:00.000Z');
- let datePickerOpenSpy;
+ const datePickerOpenSpy = jest.fn();
beforeEach(() => {
- createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
- datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker, 'show');
+ createComponent({
+ canUpdate: true,
+ dueDate: '2022-12-31',
+ startDate: '2022-12-31',
+ stubs: {
+ GlDatepicker: stubComponent(GlDatepicker, {
+ methods: {
+ show: datePickerOpenSpy,
+ },
+ }),
+ },
+ });
findStartDatePicker().vm.$emit('input', startDate);
findStartDatePicker().vm.$emit('close');
});
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 554c9a4f7b8..6894aa236e3 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -266,7 +266,7 @@ describe('WorkItemLabels component', () => {
});
it('skips calling the work item query when missing workItemIid', async () => {
- createComponent({ workItemIid: null });
+ createComponent({ workItemIid: '' });
await waitForPromises();
expect(workItemQuerySuccess).not.toHaveBeenCalled();
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
index b06be6c8083..cd077fbf705 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
@@ -6,16 +6,28 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { childrenWorkItems, workItemByIidResponseFactory } from '../../mock_data';
+import {
+ changeWorkItemParentMutationResponse,
+ childrenWorkItems,
+ updateWorkItemMutationErrorResponse,
+ workItemByIidResponseFactory,
+} from '../../mock_data';
describe('WorkItemChildrenWrapper', () => {
let wrapper;
+ const $toast = {
+ show: jest.fn(),
+ };
const getWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+ const updateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(changeWorkItemParentMutationResponse);
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
@@ -25,18 +37,33 @@ describe('WorkItemChildrenWrapper', () => {
workItemType = 'Objective',
confidential = false,
children = childrenWorkItems,
+ mutationHandler = updateWorkItemMutationHandler,
} = {}) => {
+ const mockApollo = createMockApollo([
+ [workItemByIidQuery, getWorkItemQueryHandler],
+ [updateWorkItemMutation, mutationHandler],
+ ]);
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: workItemByIidQuery,
+ variables: { fullPath: 'test/project', iid: '1' },
+ data: workItemByIidResponseFactory().data,
+ });
+
wrapper = shallowMountExtended(WorkItemChildrenWrapper, {
- apolloProvider: createMockApollo([[workItemByIidQuery, getWorkItemQueryHandler]]),
+ apolloProvider: mockApollo,
provide: {
fullPath: 'test/project',
},
propsData: {
workItemType,
workItemId: 'gid://gitlab/WorkItem/515',
+ workItemIid: '1',
confidential,
children,
- fetchByIid: true,
+ },
+ mocks: {
+ $toast,
},
});
};
@@ -51,16 +78,6 @@ describe('WorkItemChildrenWrapper', () => {
);
});
- it('remove event on child triggers `removeChild` event', () => {
- createComponent();
- const workItem = { id: 'gid://gitlab/WorkItem/2' };
- const firstChild = findWorkItemLinkChildItems().at(0);
-
- firstChild.vm.$emit('removeChild', workItem);
-
- expect(wrapper.emitted('removeChild')).toEqual([[workItem]]);
- });
-
it('emits `show-modal` on `click` event', () => {
createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
@@ -95,4 +112,47 @@ describe('WorkItemChildrenWrapper', () => {
}
},
);
+
+ describe('when removing child work item', () => {
+ const workItem = { id: 'gid://gitlab/WorkItem/2' };
+
+ describe('when successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ findWorkItemLinkChildItems().at(0).vm.$emit('removeChild', workItem);
+ await waitForPromises();
+ });
+
+ it('calls a mutation to update the work item', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItem.id,
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ });
+
+ it('shows a toast', () => {
+ expect($toast.show).toHaveBeenCalledWith('Child removed', {
+ action: { onClick: expect.anything(), text: 'Undo' },
+ });
+ });
+ });
+
+ describe('when not successful', () => {
+ beforeEach(async () => {
+ createComponent({
+ mutationHandler: jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse),
+ });
+ findWorkItemLinkChildItems().at(0).vm.$emit('removeChild', workItem);
+ await waitForPromises();
+ });
+
+ it('emits an error message', () => {
+ expect(wrapper.emitted('error')).toEqual([['Something went wrong while removing child.']]);
+ });
+ });
+ });
});
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 786f8604039..dd46505bd65 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
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { stubComponent } from 'helpers/stub_component';
+import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
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';
@@ -13,19 +13,14 @@ import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/wor
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { FORM_TYPES } from '~/work_items/constants';
-import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
getIssueDetailsResponse,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
- changeWorkItemParentMutationResponse,
workItemByIidResponseFactory,
- workItemQueryResponse,
mockWorkItemCommentNote,
- childrenWorkItems,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -36,66 +31,48 @@ describe('WorkItemLinks', () => {
let wrapper;
let mockApollo;
- const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
-
- const $toast = {
- show: jest.fn(),
- };
-
- const mutationChangeParentHandler = jest
- .fn()
- .mockResolvedValue(changeWorkItemParentMutationResponse);
- const childWorkItemByIidHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse);
const responseWithoutAddChildPermission = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory({ adminParentLink: false }));
const createComponent = async ({
- data = {},
fetchHandler = responseWithAddChildPermission,
- mutationHandler = mutationChangeParentHandler,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
} = {}) => {
mockApollo = createMockApollo(
[
- [workItemQuery, fetchHandler],
- [changeWorkItemParentMutation, mutationHandler],
+ [workItemByIidQuery, fetchHandler],
[issueDetailsQuery, issueDetailsQueryHandler],
- [workItemByIidQuery, childWorkItemByIidHandler],
],
resolvers,
{ addTypename: true },
);
wrapper = shallowMountExtended(WorkItemLinks, {
- data() {
- return {
- ...data,
- };
- },
provide: {
fullPath: 'project/path',
hasIterationsFeature,
reportAbusePath: '/report/abuse/path',
},
- propsData: { issuableId: 1 },
- apolloProvider: mockApollo,
- mocks: {
- $toast,
+ propsData: {
+ issuableId: 1,
+ issuableIid: 1,
},
+ apolloProvider: mockApollo,
stubs: {
WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
methods: {
show: showModal,
},
}),
+ WidgetWrapper: stubComponent(WidgetWrapper, {
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
},
});
- wrapper.vm.$refs.wrapper.show = jest.fn();
-
await waitForPromises();
};
@@ -122,8 +99,7 @@ describe('WorkItemLinks', () => {
`(
'$expectedAssertion "Add" button in hierarchy widget header when "userPermissions.adminParentLink" is $value',
async ({ workItemFetchHandler, value }) => {
- createComponent({ fetchHandler: workItemFetchHandler });
- await waitForPromises();
+ await createComponent({ fetchHandler: workItemFetchHandler });
expect(findToggleFormDropdown().exists()).toBe(value);
},
@@ -159,24 +135,6 @@ 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(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
-
- findAddLinksForm().vm.$emit('addWorkItemChild', workItem);
- await waitForPromises();
-
- expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(5);
- });
});
describe('when no child links', () => {
@@ -230,50 +188,6 @@ describe('WorkItemLinks', () => {
});
});
- describe('remove child', () => {
- let firstChild;
-
- beforeEach(async () => {
- await createComponent({ mutationHandler: mutationChangeParentHandler });
-
- [firstChild] = childrenWorkItems;
- });
-
- it('calls correct mutation with correct variables', async () => {
- findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
-
- await waitForPromises();
-
- expect(mutationChangeParentHandler).toHaveBeenCalledWith({
- input: {
- id: WORK_ITEM_ID,
- hierarchyWidget: {
- parentId: null,
- },
- },
- });
- });
-
- it('shows toast when mutation succeeds', async () => {
- findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
-
- await waitForPromises();
-
- expect($toast.show).toHaveBeenCalledWith('Child removed', {
- action: { onClick: expect.anything(), text: 'Undo' },
- });
- });
-
- it('renders correct number of children after removal', async () => {
- expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
-
- findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
- await waitForPromises();
-
- expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(3);
- });
- });
-
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
@@ -289,16 +203,6 @@ describe('WorkItemLinks', () => {
});
});
- it('starts prefetching work item by iid if URL contains work_item_iid query parameter', async () => {
- setWindowLocation('?work_item_iid=5');
- await createComponent();
-
- expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
- iid: '5',
- fullPath: 'project/path',
- });
- });
-
it('does not open the modal if work item iid URL parameter is not found in child items', async () => {
setWindowLocation('?work_item_iid=555');
await createComponent();
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 06716584879..f3aa347f389 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
@@ -1,6 +1,7 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
@@ -19,6 +20,7 @@ describe('WorkItemTree', () => {
const findEmptyState = () => wrapper.findByTestId('tree-empty');
const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
+ const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
const createComponent = ({
@@ -70,6 +72,16 @@ describe('WorkItemTree', () => {
expect(findForm().exists()).toBe(false);
});
+ it('shows an error message on error', async () => {
+ const errorMessage = 'Some error';
+ createComponent();
+
+ findWorkItemLinkChildrenWrapper().vm.$emit('error', errorMessage);
+ await nextTick();
+
+ expect(findWidgetWrapper().props('error')).toBe(errorMessage);
+ });
+
it.each`
option | event | formType | childType
${'New objective'} | ${'showCreateObjectiveForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
diff --git a/spec/frontend/work_items/graphql/cache_utils_spec.js b/spec/frontend/work_items/graphql/cache_utils_spec.js
new file mode 100644
index 00000000000..6d0083790d1
--- /dev/null
+++ b/spec/frontend/work_items/graphql/cache_utils_spec.js
@@ -0,0 +1,153 @@
+import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
+import { addHierarchyChild, removeHierarchyChild } from '~/work_items/graphql/cache_utils';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+
+describe('work items graphql cache utils', () => {
+ const fullPath = 'full/path';
+ const iid = '10';
+ const mockCacheData = {
+ workspace: {
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/10',
+ title: 'Work item',
+ widgets: [
+ {
+ type: WIDGET_TYPE_HIERARCHY,
+ children: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/20',
+ title: 'Child',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ };
+
+ describe('addHierarchyChild', () => {
+ it('updates the work item with a new child', () => {
+ const mockCache = {
+ readQuery: () => mockCacheData,
+ writeQuery: jest.fn(),
+ };
+
+ const child = {
+ id: 'gid://gitlab/WorkItem/30',
+ title: 'New child',
+ };
+
+ addHierarchyChild(mockCache, fullPath, iid, child);
+
+ expect(mockCache.writeQuery).toHaveBeenCalledWith({
+ query: workItemByIidQuery,
+ variables: { fullPath, iid },
+ data: {
+ workspace: {
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/10',
+ title: 'Work item',
+ widgets: [
+ {
+ type: WIDGET_TYPE_HIERARCHY,
+ children: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/20',
+ title: 'Child',
+ },
+ child,
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ });
+ });
+
+ it('does not update the work item when there is no cache data', () => {
+ const mockCache = {
+ readQuery: () => {},
+ writeQuery: jest.fn(),
+ };
+
+ const child = {
+ id: 'gid://gitlab/WorkItem/30',
+ title: 'New child',
+ };
+
+ addHierarchyChild(mockCache, fullPath, iid, child);
+
+ expect(mockCache.writeQuery).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('removeHierarchyChild', () => {
+ it('updates the work item with a new child', () => {
+ const mockCache = {
+ readQuery: () => mockCacheData,
+ writeQuery: jest.fn(),
+ };
+
+ const childToRemove = {
+ id: 'gid://gitlab/WorkItem/20',
+ title: 'Child',
+ };
+
+ removeHierarchyChild(mockCache, fullPath, iid, childToRemove);
+
+ expect(mockCache.writeQuery).toHaveBeenCalledWith({
+ query: workItemByIidQuery,
+ variables: { fullPath, iid },
+ data: {
+ workspace: {
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/10',
+ title: 'Work item',
+ widgets: [
+ {
+ type: WIDGET_TYPE_HIERARCHY,
+ children: {
+ nodes: [],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ });
+ });
+
+ it('does not update the work item when there is no cache data', () => {
+ const mockCache = {
+ readQuery: () => {},
+ writeQuery: jest.fn(),
+ };
+
+ const childToRemove = {
+ id: 'gid://gitlab/WorkItem/20',
+ title: 'Child',
+ };
+
+ removeHierarchyChild(mockCache, fullPath, iid, childToRemove);
+
+ expect(mockCache.writeQuery).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 05c6a21bb38..a873462ea63 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -51,6 +51,7 @@ export const mockAwardEmojiThumbsUp = {
__typename: 'AwardEmoji',
user: {
id: 'gid://gitlab/User/5',
+ name: 'Dave Smith',
__typename: 'UserCore',
},
};
@@ -60,6 +61,7 @@ export const mockAwardEmojiThumbsDown = {
__typename: 'AwardEmoji',
user: {
id: 'gid://gitlab/User/5',
+ name: 'Dave Smith',
__typename: 'UserCore',
},
};
@@ -95,6 +97,7 @@ export const workItemQueryResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
workItemType: {
__typename: 'WorkItemType',
@@ -107,6 +110,7 @@ export const workItemQueryResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -198,6 +202,7 @@ export const updateWorkItemMutationResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
workItemType: {
__typename: 'WorkItemType',
@@ -210,8 +215,12 @@ export const updateWorkItemMutationResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
+ reference: 'test-project-path#1',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com',
widgets: [
{
type: 'HIERARCHY',
@@ -302,6 +311,7 @@ export const convertWorkItemMutationResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
workItemType: {
__typename: 'WorkItemType',
@@ -314,8 +324,12 @@ export const convertWorkItemMutationResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
+ reference: 'gitlab-org/gitlab-test#1',
+ createNoteEmail:
+ 'gitlab-incoming+gitlab-org-gitlab-test-2-ddpzuq0zd2wefzofcpcdr3dg7-issue-1@gmail.com',
widgets: [
{
type: 'HIERARCHY',
@@ -407,6 +421,7 @@ export const objectiveType = {
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
+ canCreateNote = false,
adminParentLink = false,
notificationsWidgetPresent = true,
currentUserTodosWidgetPresent = true,
@@ -454,6 +469,7 @@ export const workItemResponseFactory = ({
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
workItemType,
userPermissions: {
@@ -461,8 +477,12 @@ export const workItemResponseFactory = ({
updateWorkItem: canUpdate,
setWorkItemMetadata: canUpdate,
adminParentLink,
+ createNote: canCreateNote,
__typename: 'WorkItemPermissions',
},
+ reference: 'test-project-path#1',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com',
widgets: [
{
__typename: 'WorkItemWidgetDescription',
@@ -723,6 +743,7 @@ export const createWorkItemMutationResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
workItemType: {
__typename: 'WorkItemType',
@@ -735,8 +756,12 @@ export const createWorkItemMutationResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
+ reference: 'test-project-path#1',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com',
widgets: [],
},
errors: [],
@@ -928,49 +953,62 @@ export const workItemMilestoneSubscriptionResponse = {
export const workItemHierarchyEmptyResponse = {
data: {
- workItem: {
- id: 'gid://gitlab/WorkItem/1',
- iid: '1',
- state: 'OPEN',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/1',
- name: 'Issue',
- iconName: 'issue-type-issue',
- __typename: 'WorkItemType',
- },
- title: 'New title',
- description: '',
- createdAt: '2022-08-03T12:41:54Z',
- updatedAt: null,
- closedAt: null,
- author: mockAssignees[0],
- project: {
- __typename: 'Project',
- id: '1',
- fullPath: 'test-project-path',
- archived: false,
- },
- userPermissions: {
- deleteWorkItem: false,
- updateWorkItem: false,
- setWorkItemMetadata: false,
- adminParentLink: false,
- __typename: 'WorkItemPermissions',
- },
- confidential: false,
- widgets: [
- {
- type: 'HIERARCHY',
- parent: null,
- hasChildren: false,
- children: {
- nodes: [],
- __typename: 'WorkItemConnection',
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
+ state: 'OPEN',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ description: '',
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
+ author: mockAssignees[0],
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ archived: false,
+ name: 'Project name',
+ },
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ createNote: false,
+ __typename: 'WorkItemPermissions',
+ },
+ confidential: false,
+ reference: 'test-project-path#1',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com',
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ hasChildren: false,
+ children: {
+ nodes: [],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
},
- __typename: 'WorkItemWidgetHierarchy',
- },
- ],
- __typename: 'WorkItem',
+ ],
+ },
},
},
};
@@ -998,6 +1036,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
project: {
@@ -1005,6 +1044,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
confidential: false,
widgets: [
@@ -1126,51 +1166,64 @@ export const childrenWorkItems = [
export const workItemHierarchyResponse = {
data: {
- workItem: {
- id: 'gid://gitlab/WorkItem/1',
- iid: '1',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/1',
- name: 'Issue',
- iconName: 'issue-type-issue',
- __typename: 'WorkItemType',
- },
- title: 'New title',
- userPermissions: {
- deleteWorkItem: true,
- updateWorkItem: true,
- setWorkItemMetadata: true,
- adminParentLink: true,
- __typename: 'WorkItemPermissions',
- },
- author: {
- ...mockAssignees[0],
- },
- confidential: false,
- project: {
- __typename: 'Project',
- id: '1',
- fullPath: 'test-project-path',
- archived: false,
- },
- description: 'Issue description',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- updatedAt: null,
- closedAt: null,
- widgets: [
- {
- type: 'HIERARCHY',
- parent: null,
- hasChildren: true,
- children: {
- nodes: childrenWorkItems,
- __typename: 'WorkItemConnection',
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ setWorkItemMetadata: true,
+ adminParentLink: true,
+ createNote: true,
+ __typename: 'WorkItemPermissions',
+ },
+ author: {
+ ...mockAssignees[0],
+ },
+ confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ archived: false,
+ name: 'Project name',
+ },
+ description: 'Issue description',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
+ reference: 'test-project-path#1',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com',
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ hasChildren: true,
+ children: {
+ nodes: childrenWorkItems,
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
},
- __typename: 'WorkItemWidgetHierarchy',
- },
- ],
- __typename: 'WorkItem',
+ ],
+ },
},
},
};
@@ -1226,12 +1279,14 @@ export const workItemObjectiveWithChild = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1301,6 +1356,7 @@ export const workItemHierarchyTreeResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1309,6 +1365,7 @@ export const workItemHierarchyTreeResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
widgets: [
{
@@ -1380,6 +1437,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
description: null,
@@ -1399,7 +1457,11 @@ export const changeIndirectWorkItemParentMutationResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
+ reference: 'test-project-path#13',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-13@gmail.com',
widgets: [
{
__typename: 'WorkItemWidgetHierarchy',
@@ -1443,6 +1505,7 @@ export const changeWorkItemParentMutationResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
description: null,
@@ -1462,7 +1525,11 @@ export const changeWorkItemParentMutationResponse = {
id: '1',
fullPath: 'test-project-path',
archived: false,
+ name: 'Project name',
},
+ reference: 'test-project-path#2',
+ createNoteEmail:
+ 'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-2@gmail.com',
widgets: [
{
__typename: 'WorkItemWidgetHierarchy',
@@ -1561,6 +1628,74 @@ export const projectMembersResponseWithCurrentUser = {
},
};
+export const projectMembersResponseWithDuplicates = {
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ users: {
+ nodes: [
+ {
+ id: 'user-2',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/5',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+ },
+ },
+ {
+ id: 'user-4',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/5',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+ },
+ },
+ {
+ id: 'user-1',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ },
+ {
+ id: 'user-3',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ startCursor: null,
+ },
+ },
+ },
+ },
+};
+
export const projectMembersResponseWithCurrentUserWithNextPage = {
data: {
workspace: {
@@ -1867,6 +2002,8 @@ export const mockWorkItemNotesResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
},
@@ -1879,6 +2016,10 @@ export const mockWorkItemNotesResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/36',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -1912,6 +2053,8 @@ export const mockWorkItemNotesResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723565678',
},
@@ -1924,6 +2067,10 @@ export const mockWorkItemNotesResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/76',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -1956,6 +2103,8 @@ export const mockWorkItemNotesResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
},
@@ -1968,6 +2117,10 @@ export const mockWorkItemNotesResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/71',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2060,6 +2213,8 @@ export const mockWorkItemNotesByIidResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: null,
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
@@ -2073,6 +2228,10 @@ export const mockWorkItemNotesByIidResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/72',
+ descriptionVersion: null,
+ },
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -2107,6 +2266,8 @@ export const mockWorkItemNotesByIidResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: null,
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765',
@@ -2120,6 +2281,10 @@ export const mockWorkItemNotesByIidResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/76',
+ descriptionVersion: null,
+ },
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -2155,6 +2320,8 @@ export const mockWorkItemNotesByIidResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: null,
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
@@ -2168,6 +2335,10 @@ export const mockWorkItemNotesByIidResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/22',
+ descriptionVersion: null,
+ },
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -2261,6 +2432,8 @@ export const mockMoreWorkItemNotesResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
@@ -2274,6 +2447,10 @@ export const mockMoreWorkItemNotesResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/16',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2308,6 +2485,8 @@ export const mockMoreWorkItemNotesResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
@@ -2321,6 +2500,10 @@ export const mockMoreWorkItemNotesResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/96',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2353,6 +2536,8 @@ export const mockMoreWorkItemNotesResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
@@ -2366,6 +2551,10 @@ export const mockMoreWorkItemNotesResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/56',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2417,6 +2606,8 @@ export const createWorkItemNoteResponse = {
lastEditedAt: null,
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
__typename: 'Discussion',
@@ -2430,6 +2621,7 @@ export const createWorkItemNoteResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ systemNoteMetadata: null,
userPermissions: {
adminNote: true,
awardEmoji: true,
@@ -2467,6 +2659,8 @@ export const mockWorkItemCommentNote = {
lastEditedBy: null,
system: false,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
},
@@ -2479,6 +2673,7 @@ export const mockWorkItemCommentNote = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: null,
author: {
avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 'gid://gitlab/User/1',
@@ -2489,6 +2684,16 @@ export const mockWorkItemCommentNote = {
},
};
+export const mockWorkItemCommentNoteByContributor = {
+ ...mockWorkItemCommentNote,
+ authorIsContributor: true,
+};
+
+export const mockWorkItemCommentByMaintainer = {
+ ...mockWorkItemCommentNote,
+ maxAccessLevelOfAuthor: 'Maintainer',
+};
+
export const mockWorkItemNotesResponseWithComments = {
data: {
workspace: {
@@ -2550,6 +2755,8 @@ export const mockWorkItemNotesResponseWithComments = {
url:
'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
@@ -2564,6 +2771,7 @@ export const mockWorkItemNotesResponseWithComments = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ systemNoteMetadata: null,
userPermissions: {
adminNote: true,
awardEmoji: true,
@@ -2587,6 +2795,8 @@ export const mockWorkItemNotesResponseWithComments = {
url:
'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
@@ -2601,6 +2811,7 @@ export const mockWorkItemNotesResponseWithComments = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ systemNoteMetadata: null,
userPermissions: {
adminNote: true,
awardEmoji: true,
@@ -2633,6 +2844,8 @@ export const mockWorkItemNotesResponseWithComments = {
lastEditedBy: null,
system: false,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id:
'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
@@ -2646,6 +2859,7 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: null,
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2704,6 +2918,8 @@ export const workItemNotesCreateSubscriptionResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
},
@@ -2716,6 +2932,10 @@ export const workItemNotesCreateSubscriptionResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/65',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2739,6 +2959,10 @@ export const workItemNotesCreateSubscriptionResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/26',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2766,6 +2990,8 @@ export const workItemNotesUpdateSubscriptionResponse = {
lastEditedBy: null,
system: true,
internal: false,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
},
@@ -2778,6 +3004,10 @@ export const workItemNotesUpdateSubscriptionResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/46',
+ descriptionVersion: null,
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -2801,3 +3031,322 @@ export const workItemNotesDeleteSubscriptionResponse = {
},
},
};
+
+export const workItemSystemNoteWithMetadata = {
+ id: 'gid://gitlab/Note/1651',
+ body: 'changed the description',
+ bodyHtml: '<p data-sourcepos="1:1-1:23" dir="auto">changed the description</p>',
+ system: true,
+ internal: false,
+ systemNoteIconName: 'pencil',
+ createdAt: '2023-05-05T07:19:37Z',
+ lastEditedAt: '2023-05-05T07:19:37Z',
+ url: 'https://gdk.test:3443/flightjs/Flight/-/work_items/46#note_1651',
+ lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/7d4a46ea0525e2eeed451f7b718b0ebe73205374',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'https://gdk.test:3443/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: false,
+ __typename: 'NotePermissions',
+ },
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/670',
+ descriptionVersion: {
+ id: 'gid://gitlab/DescriptionVersion/167',
+ description: '5th May 90 987',
+ diff: '<span class="idiff">5th May 90</span><span class="idiff addition"> 987</span>',
+ diffPath: '/flightjs/Flight/-/issues/46/descriptions/167/diff',
+ deletePath: '/flightjs/Flight/-/issues/46/descriptions/167',
+ canDelete: true,
+ deleted: false,
+ startVersionId: '',
+ __typename: 'DescriptionVersion',
+ },
+ __typename: 'SystemNoteMetadata',
+ },
+ __typename: 'Note',
+};
+
+export const workItemNotesWithSystemNotesWithChangedDescription = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/4',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/733',
+ iid: '79',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id: 'gid://gitlab/Discussion/aa72f4c2f3eef66afa6d79a805178801ce4bd89f',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/1687',
+ body: 'changed the description',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed the description</p>',
+ system: true,
+ internal: false,
+ systemNoteIconName: 'pencil',
+ createdAt: '2023-05-10T05:21:01Z',
+ lastEditedAt: '2023-05-10T05:21:01Z',
+ url: 'https://gdk.test:3443/gnuwget/Wget2/-/work_items/79#note_1687',
+ lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/aa72f4c2f3eef66afa6d79a805178801ce4bd89f',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'https://gdk.test:3443/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: false,
+ __typename: 'NotePermissions',
+ },
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/703',
+ descriptionVersion: {
+ id: 'gid://gitlab/DescriptionVersion/198',
+ description: 'Desc1',
+ diff: '<span class="idiff addition">Desc1</span>',
+ diffPath: '/gnuwget/Wget2/-/issues/79/descriptions/198/diff',
+ deletePath: '/gnuwget/Wget2/-/issues/79/descriptions/198',
+ canDelete: true,
+ deleted: false,
+ __typename: 'DescriptionVersion',
+ },
+ __typename: 'SystemNoteMetadata',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/a7d3cf7bd72f7a98f802845f538af65cb11a02cc',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/1688',
+ body: 'changed the description',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed the description</p>',
+ system: true,
+ internal: false,
+ systemNoteIconName: 'pencil',
+ createdAt: '2023-05-10T05:21:05Z',
+ lastEditedAt: '2023-05-10T05:21:05Z',
+ url: 'https://gdk.test:3443/gnuwget/Wget2/-/work_items/79#note_1688',
+ lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/a7d3cf7bd72f7a98f802845f538af65cb11a02cc',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'https://gdk.test:3443/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: false,
+ __typename: 'NotePermissions',
+ },
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/704',
+ descriptionVersion: {
+ id: 'gid://gitlab/DescriptionVersion/199',
+ description: 'Desc2',
+ diff:
+ '<span class="idiff">Desc</span><span class="idiff deletion">1</span><span class="idiff addition">2</span>',
+ diffPath: '/gnuwget/Wget2/-/issues/79/descriptions/199/diff',
+ deletePath: '/gnuwget/Wget2/-/issues/79/descriptions/199',
+ canDelete: true,
+ deleted: false,
+ __typename: 'DescriptionVersion',
+ },
+ __typename: 'SystemNoteMetadata',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/391eed1ee0a258cc966a51dde900424f3b51b95d',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/1689',
+ body: 'changed the description',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed the description</p>',
+ system: true,
+ internal: false,
+ systemNoteIconName: 'pencil',
+ createdAt: '2023-05-10T05:21:08Z',
+ lastEditedAt: '2023-05-10T05:21:08Z',
+ url: 'https://gdk.test:3443/gnuwget/Wget2/-/work_items/79#note_1689',
+ lastEditedBy: null,
+ maxAccessLevelOfAuthor: 'Owner',
+ authorIsContributor: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/391eed1ee0a258cc966a51dde900424f3b51b95d',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'https://gdk.test:3443/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: false,
+ __typename: 'NotePermissions',
+ },
+ systemNoteMetadata: {
+ id: 'gid://gitlab/SystemNoteMetadata/705',
+ descriptionVersion: {
+ id: 'gid://gitlab/DescriptionVersion/200',
+ description: 'Desc3',
+ diff:
+ '<span class="idiff">Desc</span><span class="idiff deletion">2</span><span class="idiff addition">3</span>',
+ diffPath: '/gnuwget/Wget2/-/issues/79/descriptions/200/diff',
+ deletePath: '/gnuwget/Wget2/-/issues/79/descriptions/200',
+ canDelete: true,
+ deleted: false,
+ __typename: 'DescriptionVersion',
+ },
+ __typename: 'SystemNoteMetadata',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ {
+ __typename: 'WorkItemWidgetHealthStatus',
+ },
+ {
+ __typename: 'WorkItemWidgetProgress',
+ },
+ {
+ __typename: 'WorkItemWidgetNotifications',
+ },
+ {
+ __typename: 'WorkItemWidgetCurrentUserTodos',
+ },
+ {
+ __typename: 'WorkItemWidgetAwardEmoji',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const getAwardEmojiResponse = (toggledOn) => {
+ return {
+ data: {
+ awardEmojiToggle: {
+ errors: [],
+ toggledOn,
+ },
+ },
+ };
+};
diff --git a/spec/frontend/work_items/notes/collapse_utils_spec.js b/spec/frontend/work_items/notes/collapse_utils_spec.js
new file mode 100644
index 00000000000..c26ef891e9f
--- /dev/null
+++ b/spec/frontend/work_items/notes/collapse_utils_spec.js
@@ -0,0 +1,29 @@
+import {
+ isDescriptionSystemNote,
+ getTimeDifferenceInMinutes,
+} from '~/work_items/notes/collapse_utils';
+import { workItemSystemNoteWithMetadata } from '../mock_data';
+
+describe('Work items collapse utils', () => {
+ it('checks if a system note is of a description type', () => {
+ expect(isDescriptionSystemNote(workItemSystemNoteWithMetadata)).toEqual(true);
+ });
+
+ it('returns false when a system note is not a description type', () => {
+ expect(isDescriptionSystemNote({ ...workItemSystemNoteWithMetadata, system: false })).toEqual(
+ false,
+ );
+ });
+
+ it('gets the time difference between two notes', () => {
+ const anotherSystemNote = {
+ ...workItemSystemNoteWithMetadata,
+ createdAt: '2023-05-06T07:19:37Z',
+ };
+
+ // kept the dates 24 hours apart so 24 * 60 mins = 1440
+ expect(getTimeDifferenceInMinutes(workItemSystemNoteWithMetadata, anotherSystemNote)).toEqual(
+ 1440,
+ );
+ });
+});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index c480affe484..84b10f30418 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -34,7 +34,7 @@ describe('Work items root component', () => {
issuesListPath,
},
propsData: {
- id: '1',
+ iid: '1',
},
mocks: {
$toast: {
@@ -49,7 +49,6 @@ describe('Work items root component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: false,
- workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
workItemIid: '1',
});