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