diff options
Diffstat (limited to 'spec/frontend/admin/abuse_report/components/notes')
6 files changed, 843 insertions, 4 deletions
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js new file mode 100644 index 00000000000..959b52beaef --- /dev/null +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js @@ -0,0 +1,227 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createAlert } from '~/alert'; +import { clearDraft } from '~/lib/utils/autosave'; +import waitForPromises from 'helpers/wait_for_promises'; +import createNoteMutation from '~/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql'; +import AbuseReportAddNote from '~/admin/abuse_report/components/notes/abuse_report_add_note.vue'; +import AbuseReportCommentForm from '~/admin/abuse_report/components/notes/abuse_report_comment_form.vue'; + +import { mockAbuseReport, createAbuseReportNoteResponse } from '../../mock_data'; + +jest.mock('~/alert'); +jest.mock('~/lib/utils/autosave'); +Vue.use(VueApollo); + +describe('Abuse Report Add Note', () => { + let wrapper; + + const mockAbuseReportId = mockAbuseReport.report.globalId; + const mockDiscussionId = 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a'; + + const mutationSuccessHandler = jest.fn().mockResolvedValue(createAbuseReportNoteResponse); + + const findTimelineEntry = () => wrapper.findByTestId('abuse-report-note-timeline-entry'); + const findTimelineEntryInner = () => + wrapper.findByTestId('abuse-report-note-timeline-entry-inner'); + const findCommentFormWrapper = () => wrapper.findByTestId('abuse-report-comment-form-wrapper'); + + const findAbuseReportCommentForm = () => wrapper.findComponent(AbuseReportCommentForm); + const findReplyTextarea = () => wrapper.findByTestId('abuse-report-note-reply-textarea'); + + const createComponent = ({ + mutationHandler = mutationSuccessHandler, + abuseReportId = mockAbuseReportId, + discussionId = '', + isNewDiscussion = true, + showCommentForm = false, + } = {}) => { + wrapper = shallowMountExtended(AbuseReportAddNote, { + apolloProvider: createMockApollo([[createNoteMutation, mutationHandler]]), + propsData: { + abuseReportId, + discussionId, + isNewDiscussion, + showCommentForm, + }, + }); + }; + + describe('Default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should show the comment form', () => { + expect(findAbuseReportCommentForm().exists()).toBe(true); + expect(findAbuseReportCommentForm().props()).toMatchObject({ + abuseReportId: mockAbuseReportId, + isSubmitting: false, + autosaveKey: `${mockAbuseReportId}-comment`, + commentButtonText: 'Comment', + initialValue: '', + }); + }); + + it('should not show the reply textarea', () => { + expect(findReplyTextarea().exists()).toBe(false); + }); + + it('should add the correct classList to timeline-entry', () => { + expect(findTimelineEntry().classes()).toEqual( + expect.arrayContaining(['timeline-entry', 'note-form']), + ); + + expect(findTimelineEntryInner().classes()).toEqual(['timeline-entry-inner']); + }); + }); + + describe('When the main comments has replies', () => { + beforeEach(() => { + createComponent({ + discussionId: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', + isNewDiscussion: false, + }); + }); + + it('should add the correct classLists', () => { + expect(findTimelineEntry().classes()).toEqual( + expect.arrayContaining([ + 'note', + 'note-wrapper', + 'note-comment', + 'discussion-reply-holder', + 'gl-border-t-0!', + 'clearfix', + ]), + ); + + expect(findTimelineEntryInner().classes()).toEqual([]); + + expect(findCommentFormWrapper().classes()).toEqual( + expect.arrayContaining([ + 'gl-relative', + 'gl-display-flex', + 'gl-align-items-flex-start', + 'gl-flex-nowrap', + ]), + ); + }); + + it('should show not the comment form', () => { + expect(findAbuseReportCommentForm().exists()).toBe(false); + }); + + it('should show the reply textarea', () => { + expect(findReplyTextarea().exists()).toBe(true); + expect(findReplyTextarea().attributes()).toMatchObject({ + rows: '1', + placeholder: 'Reply…', + 'aria-label': 'Reply to comment', + }); + }); + }); + + describe('Adding a comment', () => { + const noteText = 'mock note'; + + beforeEach(() => { + createComponent(); + + findAbuseReportCommentForm().vm.$emit('submitForm', { + commentText: noteText, + }); + }); + + it('should call the mutation with provided noteText', async () => { + expect(findAbuseReportCommentForm().props('isSubmitting')).toBe(true); + + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + noteableId: mockAbuseReportId, + body: noteText, + discussionId: null, + }, + }); + + await waitForPromises(); + + expect(findAbuseReportCommentForm().props('isSubmitting')).toBe(false); + }); + + it('should add the correct classList to comment-form wrapper', () => { + expect(findCommentFormWrapper().classes()).toEqual([]); + }); + + it('should clear draft from local storage', async () => { + await waitForPromises(); + + expect(clearDraft).toHaveBeenCalledWith(`${mockAbuseReportId}-comment`); + }); + + it('should emit `cancelEditing` event', async () => { + await waitForPromises(); + + expect(wrapper.emitted('cancelEditing')).toHaveLength(1); + }); + + it.each` + description | errorResponse + ${'with an error response'} | ${new Error('The discussion could not be found')} + ${'without an error ressponse'} | ${null} + `('should show an error when mutation fails $description', async ({ errorResponse }) => { + createComponent({ + mutationHandler: jest.fn().mockRejectedValue(errorResponse), + }); + + findAbuseReportCommentForm().vm.$emit('submitForm', { + commentText: noteText, + }); + + await waitForPromises(); + + const errorMessage = errorResponse + ? 'Your comment could not be submitted because the discussion could not be found.' + : 'Your comment could not be submitted! Please check your network connection and try again.'; + + expect(createAlert).toHaveBeenCalledWith({ + message: errorMessage, + captureError: true, + parent: expect.anything(), + }); + }); + }); + + describe('Replying to a comment', () => { + beforeEach(() => { + createComponent({ + discussionId: mockDiscussionId, + isNewDiscussion: false, + showCommentForm: false, + }); + }); + + it('should not show the comment form', () => { + expect(findAbuseReportCommentForm().exists()).toBe(false); + }); + + it('should show comment form when reply textarea is clicked on', async () => { + await findReplyTextarea().trigger('click'); + + expect(findAbuseReportCommentForm().exists()).toBe(true); + expect(findAbuseReportCommentForm().props('commentButtonText')).toBe('Reply'); + }); + + it('should show comment form if `showCommentForm` is true', () => { + createComponent({ + discussionId: mockDiscussionId, + isNewDiscussion: false, + showCommentForm: true, + }); + + expect(findAbuseReportCommentForm().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js new file mode 100644 index 00000000000..2265ef7d441 --- /dev/null +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js @@ -0,0 +1,214 @@ +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys'; +import * as autosave from '~/lib/utils/autosave'; +import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; + +import AbuseReportCommentForm from '~/admin/abuse_report/components/notes/abuse_report_comment_form.vue'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; + +import { mockAbuseReport } from '../../mock_data'; + +jest.mock('~/lib/utils/autosave', () => ({ + updateDraft: jest.fn(), + clearDraft: jest.fn(), + getDraft: jest.fn().mockReturnValue(''), +})); + +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({ + confirmAction: jest.fn().mockResolvedValue(true), +})); + +describe('Abuse Report Comment Form', () => { + let wrapper; + + const mockAbuseReportId = mockAbuseReport.report.globalId; + const mockAutosaveKey = `${mockAbuseReportId}-comment`; + const mockInitialValue = 'note text'; + + const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); + const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]'); + const findCommentButton = () => wrapper.find('[data-testid="comment-button"]'); + + const createComponent = ({ + abuseReportId = mockAbuseReportId, + isSubmitting = false, + initialValue = mockInitialValue, + autosaveKey = mockAutosaveKey, + commentButtonText = 'Comment', + } = {}) => { + wrapper = shallowMount(AbuseReportCommentForm, { + propsData: { + abuseReportId, + isSubmitting, + initialValue, + autosaveKey, + commentButtonText, + }, + provide: { + uploadNoteAttachmentPath: 'test-upload-path', + }, + }); + }; + + describe('Markdown editor', () => { + it('should show markdown editor', () => { + createComponent(); + + expect(findMarkdownEditor().exists()).toBe(true); + + expect(findMarkdownEditor().props()).toMatchObject({ + value: mockInitialValue, + renderMarkdownPath: '', + uploadsPath: 'test-upload-path', + enableContentEditor: false, + formFieldProps: { + 'aria-label': 'Add a reply', + placeholder: 'Write a comment or drag your files here…', + id: 'abuse-report-add-or-edit-comment', + name: 'abuse-report-add-or-edit-comment', + }, + markdownDocsPath: '/help/user/markdown', + }); + }); + + it('should pass the draft from local storage if it exists', () => { + jest.spyOn(autosave, 'getDraft').mockImplementation(() => 'draft comment'); + createComponent(); + + expect(findMarkdownEditor().props('value')).toBe('draft comment'); + }); + + it('should pass an empty string if both draft and initialValue are empty', () => { + jest.spyOn(autosave, 'getDraft').mockImplementation(() => ''); + createComponent({ initialValue: '' }); + + expect(findMarkdownEditor().props('value')).toBe(''); + }); + }); + + describe('Markdown Editor input', () => { + beforeEach(() => { + createComponent(); + }); + + it('should set the correct comment text value', async () => { + findMarkdownEditor().vm.$emit('input', 'new comment'); + await nextTick(); + + expect(findMarkdownEditor().props('value')).toBe('new comment'); + }); + + it('should call `updateDraft` with correct parameters', () => { + findMarkdownEditor().vm.$emit('input', 'new comment'); + + expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment'); + }); + }); + + describe('Submitting a comment', () => { + beforeEach(() => { + jest.spyOn(autosave, 'getDraft').mockImplementation(() => 'draft comment'); + createComponent(); + }); + + it('should show comment button', () => { + expect(findCommentButton().exists()).toBe(true); + expect(findCommentButton().text()).toBe('Comment'); + }); + + it('should show `Reply` button if its not a new discussion', () => { + createComponent({ commentButtonText: 'Reply' }); + expect(findCommentButton().text()).toBe('Reply'); + }); + + describe('when enter with meta key is pressed', () => { + beforeEach(() => { + findMarkdownEditor().vm.$emit( + 'keydown', + new KeyboardEvent('keydown', { key: ENTER_KEY, metaKey: true }), + ); + }); + + it('should emit `submitForm` event with correct parameters', () => { + expect(wrapper.emitted('submitForm')).toEqual([[{ commentText: 'draft comment' }]]); + }); + }); + + describe('when ctrl+enter is pressed', () => { + beforeEach(() => { + findMarkdownEditor().vm.$emit( + 'keydown', + new KeyboardEvent('keydown', { key: ENTER_KEY, ctrlKey: true }), + ); + }); + + it('should emit `submitForm` event with correct parameters', () => { + expect(wrapper.emitted('submitForm')).toEqual([[{ commentText: 'draft comment' }]]); + }); + }); + + describe('when comment button is clicked', () => { + beforeEach(() => { + findCommentButton().vm.$emit('click'); + }); + + it('should emit `submitForm` event with correct parameters', () => { + expect(wrapper.emitted('submitForm')).toEqual([[{ commentText: 'draft comment' }]]); + }); + }); + }); + + describe('Cancel editing', () => { + beforeEach(() => { + jest.spyOn(autosave, 'getDraft').mockImplementation(() => 'draft comment'); + createComponent(); + }); + + it('should show cancel button', () => { + expect(findCancelButton().exists()).toBe(true); + expect(findCancelButton().text()).toBe('Cancel'); + }); + + describe('when escape key is pressed', () => { + beforeEach(() => { + findMarkdownEditor().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY })); + + return waitForPromises(); + }); + + it('should confirm a user action if comment text is not empty', () => { + expect(confirmViaGlModal.confirmAction).toHaveBeenCalled(); + }); + + it('should clear draft from local storage', () => { + expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey); + }); + + it('should emit `cancelEditing` event', () => { + expect(wrapper.emitted('cancelEditing')).toHaveLength(1); + }); + }); + + describe('when cancel button is clicked', () => { + beforeEach(() => { + findCancelButton().vm.$emit('click'); + + return waitForPromises(); + }); + + it('should confirm a user action if comment text is not empty', () => { + expect(confirmViaGlModal.confirmAction).toHaveBeenCalled(); + }); + + it('should clear draft from local storage', () => { + expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey); + }); + + it('should emit `cancelEditing` event', () => { + expect(wrapper.emitted('cancelEditing')).toHaveLength(1); + }); + }); + }); +}); diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js index 86f0939a938..fdc049725a4 100644 --- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js @@ -4,6 +4,7 @@ import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import AbuseReportDiscussion from '~/admin/abuse_report/components/notes/abuse_report_discussion.vue'; import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue'; +import AbuseReportAddNote from '~/admin/abuse_report/components/notes/abuse_report_add_note.vue'; import { mockAbuseReport, @@ -19,6 +20,7 @@ describe('Abuse Report Discussion', () => { const findAbuseReportNotes = () => wrapper.findAllComponents(AbuseReportNote); const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem); const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget); + const findAbuseReportAddNote = () => wrapper.findComponent(AbuseReportAddNote); const createComponent = ({ discussion = mockDiscussionWithNoReplies, @@ -43,6 +45,7 @@ describe('Abuse Report Discussion', () => { expect(findAbuseReportNote().props()).toMatchObject({ abuseReportId: mockAbuseReportId, note: mockDiscussionWithNoReplies[0], + showReplyButton: true, }); }); @@ -50,9 +53,13 @@ describe('Abuse Report Discussion', () => { expect(findTimelineEntryItem().exists()).toBe(false); }); - it('should not show the the toggle replies widget wrapper when no replies', () => { + it('should not show the toggle replies widget wrapper when there are no replies', () => { expect(findToggleRepliesWidget().exists()).toBe(false); }); + + it('should not show the comment form there are no replies', () => { + expect(findAbuseReportAddNote().exists()).toBe(false); + }); }); describe('When the main comments has replies', () => { @@ -75,5 +82,68 @@ describe('Abuse Report Discussion', () => { await nextTick(); expect(findAbuseReportNotes()).toHaveLength(1); }); + + it('should show the comment form', () => { + expect(findAbuseReportAddNote().exists()).toBe(true); + + expect(findAbuseReportAddNote().props()).toMatchObject({ + abuseReportId: mockAbuseReportId, + discussionId: mockDiscussionWithReplies[0].discussion.id, + isNewDiscussion: false, + }); + }); + + it('should show the reply button only for the main comment', () => { + expect(findAbuseReportNotes().at(0).props('showReplyButton')).toBe(true); + + expect(findAbuseReportNotes().at(1).props('showReplyButton')).toBe(false); + expect(findAbuseReportNotes().at(2).props('showReplyButton')).toBe(false); + }); + }); + + describe('Replying to a comment when it has no replies', () => { + beforeEach(() => { + createComponent(); + }); + + it('should show comment form when `startReplying` is emitted', async () => { + expect(findAbuseReportAddNote().exists()).toBe(false); + + findAbuseReportNote().vm.$emit('startReplying'); + await nextTick(); + + expect(findAbuseReportAddNote().exists()).toBe(true); + expect(findAbuseReportAddNote().props('showCommentForm')).toBe(true); + }); + + it('should hide the comment form when `cancelEditing` is emitted', async () => { + findAbuseReportNote().vm.$emit('startReplying'); + await nextTick(); + + findAbuseReportAddNote().vm.$emit('cancelEditing'); + await nextTick(); + + expect(findAbuseReportAddNote().exists()).toBe(false); + }); + }); + + describe('Replying to a comment with replies', () => { + beforeEach(() => { + createComponent({ + discussion: mockDiscussionWithReplies, + }); + }); + + it('should show reply textarea, but not comment form', () => { + expect(findAbuseReportAddNote().exists()).toBe(true); + expect(findAbuseReportAddNote().props('showCommentForm')).toBe(false); + }); + + it('should show comment form when reply button on main comment is clicked', async () => { + findAbuseReportNotes().at(0).vm.$emit('startReplying'); + await nextTick(); + + expect(findAbuseReportAddNote().props('showCommentForm')).toBe(true); + }); }); }); diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_edit_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_edit_note_spec.js new file mode 100644 index 00000000000..88f243b2501 --- /dev/null +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_edit_note_spec.js @@ -0,0 +1,129 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createAlert } from '~/alert'; +import { clearDraft } from '~/lib/utils/autosave'; +import waitForPromises from 'helpers/wait_for_promises'; +import updateNoteMutation from '~/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql'; +import AbuseReportEditNote from '~/admin/abuse_report/components/notes/abuse_report_edit_note.vue'; +import AbuseReportCommentForm from '~/admin/abuse_report/components/notes/abuse_report_comment_form.vue'; + +import { + mockAbuseReport, + mockDiscussionWithNoReplies, + editAbuseReportNoteResponse, +} from '../../mock_data'; + +jest.mock('~/alert'); +jest.mock('~/lib/utils/autosave'); +Vue.use(VueApollo); + +describe('Abuse Report Edit Note', () => { + let wrapper; + + const mockAbuseReportId = mockAbuseReport.report.globalId; + const mockNote = mockDiscussionWithNoReplies[0]; + + const mutationSuccessHandler = jest.fn().mockResolvedValue(editAbuseReportNoteResponse); + + const findAbuseReportCommentForm = () => wrapper.findComponent(AbuseReportCommentForm); + + const createComponent = ({ + mutationHandler = mutationSuccessHandler, + abuseReportId = mockAbuseReportId, + discussionId = '', + note = mockNote, + } = {}) => { + wrapper = shallowMountExtended(AbuseReportEditNote, { + apolloProvider: createMockApollo([[updateNoteMutation, mutationHandler]]), + propsData: { + abuseReportId, + discussionId, + note, + }, + }); + }; + + describe('Default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should show the comment form', () => { + expect(findAbuseReportCommentForm().exists()).toBe(true); + expect(findAbuseReportCommentForm().props()).toMatchObject({ + abuseReportId: mockAbuseReportId, + isSubmitting: false, + autosaveKey: `${mockNote.id}-comment`, + commentButtonText: 'Save comment', + initialValue: mockNote.body, + }); + }); + }); + + describe('Editing a comment', () => { + const noteText = 'Updated comment'; + + beforeEach(() => { + createComponent(); + + findAbuseReportCommentForm().vm.$emit('submitForm', { + commentText: noteText, + }); + }); + + it('should call the mutation with provided noteText', async () => { + expect(findAbuseReportCommentForm().props('isSubmitting')).toBe(true); + + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: mockNote.id, + body: noteText, + }, + }); + + await waitForPromises(); + + expect(findAbuseReportCommentForm().props('isSubmitting')).toBe(false); + }); + + it('should clear draft from local storage', async () => { + await waitForPromises(); + + expect(clearDraft).toHaveBeenCalledWith(`${mockNote.id}-comment`); + }); + + it('should emit `cancelEditing` event', async () => { + await waitForPromises(); + + expect(wrapper.emitted('cancelEditing')).toHaveLength(1); + }); + + it.each` + description | errorResponse + ${'with an error response'} | ${new Error('The note could not be found')} + ${'without an error ressponse'} | ${null} + `('should show an error when mutation fails $description', async ({ errorResponse }) => { + createComponent({ + mutationHandler: jest.fn().mockRejectedValue(errorResponse), + }); + + findAbuseReportCommentForm().vm.$emit('submitForm', { + commentText: noteText, + }); + + await waitForPromises(); + + const errorMessage = errorResponse + ? 'Your comment could not be updated because the note could not be found.' + : 'Something went wrong while editing your comment. Please try again.'; + + expect(createAlert).toHaveBeenCalledWith({ + message: errorMessage, + captureError: true, + parent: wrapper.vm.$el, + }); + }); + }); +}); diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js new file mode 100644 index 00000000000..1ddfb6145fc --- /dev/null +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js @@ -0,0 +1,79 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective } from 'helpers/vue_mock_directive'; +import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; +import AbuseReportNoteActions from '~/admin/abuse_report/components/notes/abuse_report_note_actions.vue'; + +describe('Abuse Report Note Actions', () => { + let wrapper; + + const findReplyButton = () => wrapper.findComponent(ReplyButton); + const findEditButton = () => wrapper.findComponent(GlButton); + + const createComponent = ({ showReplyButton = true, showEditButton = true } = {}) => { + wrapper = shallowMount(AbuseReportNoteActions, { + propsData: { + showReplyButton, + showEditButton, + }, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, + }); + }; + + describe('Default', () => { + beforeEach(() => { + createComponent(); + }); + + it('should show reply button', () => { + expect(findReplyButton().exists()).toBe(true); + }); + + it('should emit `startReplying` when reply button is clicked', () => { + findReplyButton().vm.$emit('startReplying'); + + expect(wrapper.emitted('startReplying')).toHaveLength(1); + }); + + it('should show edit button', () => { + expect(findEditButton().exists()).toBe(true); + expect(findEditButton().attributes()).toMatchObject({ + icon: 'pencil', + title: 'Edit comment', + 'aria-label': 'Edit comment', + }); + }); + + it('should emit `startEditing` when edit button is clicked', () => { + findEditButton().vm.$emit('click'); + + expect(wrapper.emitted('startEditing')).toHaveLength(1); + }); + }); + + describe('When `showReplyButton` is false', () => { + beforeEach(() => { + createComponent({ + showReplyButton: false, + }); + }); + + it('should not show reply button', () => { + expect(findReplyButton().exists()).toBe(false); + }); + }); + + describe('When `showEditButton` is false', () => { + beforeEach(() => { + createComponent({ + showEditButton: false, + }); + }); + + it('should not show edit button', () => { + expect(findEditButton().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js index b6908853e46..bc7aa8ef5de 100644 --- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js @@ -2,7 +2,10 @@ import { shallowMount } from '@vue/test-utils'; import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue'; import NoteHeader from '~/notes/components/note_header.vue'; -import NoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue'; +import EditedAt from '~/issues/show/components/edited.vue'; +import AbuseReportNoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue'; +import AbuseReportEditNote from '~/admin/abuse_report/components/notes/abuse_report_edit_note.vue'; +import AbuseReportNoteActions from '~/admin/abuse_report/components/notes/abuse_report_note_actions.vue'; import { mockAbuseReport, mockDiscussionWithNoReplies } from '../../mock_data'; @@ -10,18 +13,29 @@ describe('Abuse Report Note', () => { let wrapper; const mockAbuseReportId = mockAbuseReport.report.globalId; const mockNote = mockDiscussionWithNoReplies[0]; + const mockShowReplyButton = true; const findAvatar = () => wrapper.findComponent(GlAvatar); const findAvatarLink = () => wrapper.findComponent(GlAvatarLink); const findNoteHeader = () => wrapper.findComponent(NoteHeader); - const findNoteBody = () => wrapper.findComponent(NoteBody); + const findNoteBody = () => wrapper.findComponent(AbuseReportNoteBody); - const createComponent = ({ note = mockNote, abuseReportId = mockAbuseReportId } = {}) => { + const findEditNote = () => wrapper.findComponent(AbuseReportEditNote); + const findEditedAt = () => wrapper.findComponent(EditedAt); + + const findNoteActions = () => wrapper.findComponent(AbuseReportNoteActions); + + const createComponent = ({ + note = mockNote, + abuseReportId = mockAbuseReportId, + showReplyButton = mockShowReplyButton, + } = {}) => { wrapper = shallowMount(AbuseReportNote, { propsData: { note, abuseReportId, + showReplyButton, }, }); }; @@ -77,4 +91,110 @@ describe('Abuse Report Note', () => { }); }); }); + + describe('Editing', () => { + it('should show edit button when resolveNote is true', () => { + createComponent({ + note: { ...mockNote, userPermissions: { resolveNote: true } }, + }); + + expect(findNoteActions().props()).toMatchObject({ + showEditButton: true, + }); + }); + + it('should not show edit button when resolveNote is false', () => { + createComponent({ + note: { ...mockNote, userPermissions: { resolveNote: false } }, + }); + + expect(findNoteActions().props()).toMatchObject({ + showEditButton: false, + }); + }); + + it('should not be in edit mode by default', () => { + expect(findEditNote().exists()).toBe(false); + }); + + it('should trigger edit mode when `startEditing` event is emitted', async () => { + await findNoteActions().vm.$emit('startEditing'); + + expect(findEditNote().exists()).toBe(true); + expect(findEditNote().props()).toMatchObject({ + abuseReportId: mockAbuseReportId, + note: mockNote, + }); + + expect(findNoteHeader().exists()).toBe(false); + expect(findNoteBody().exists()).toBe(false); + }); + + it('should hide edit mode when `cancelEditing` event is emitted', async () => { + await findNoteActions().vm.$emit('startEditing'); + await findEditNote().vm.$emit('cancelEditing'); + + expect(findEditNote().exists()).toBe(false); + + expect(findNoteHeader().exists()).toBe(true); + expect(findNoteBody().exists()).toBe(true); + }); + }); + + describe('Edited At', () => { + it('should not show edited-at if lastEditedBy is null', () => { + expect(findEditedAt().exists()).toBe(false); + }); + + it('should show edited-at if lastEditedBy is not null', () => { + createComponent({ + note: { + ...mockNote, + lastEditedBy: { name: 'user', webPath: '/user' }, + lastEditedAt: '2023-10-20T02:46:50Z', + }, + }); + + expect(findEditedAt().exists()).toBe(true); + + expect(findEditedAt().props()).toMatchObject({ + updatedAt: '2023-10-20T02:46:50Z', + updatedByName: 'user', + updatedByPath: '/user', + }); + + expect(findEditedAt().classes()).toEqual( + expect.arrayContaining(['gl-text-secondary', 'gl-pl-3']), + ); + }); + + it('should add the correct classList when showReplyButton is false', () => { + createComponent({ + note: { + ...mockNote, + lastEditedBy: { name: 'user', webPath: '/user' }, + lastEditedAt: '2023-10-20T02:46:50Z', + }, + showReplyButton: false, + }); + + expect(findEditedAt().classes()).toEqual( + expect.arrayContaining(['gl-text-secondary', 'gl-pl-8']), + ); + }); + }); + + describe('Replying', () => { + it('should show reply button', () => { + expect(findNoteActions().props()).toMatchObject({ + showReplyButton: true, + }); + }); + + it('should bubble up `startReplying` event', () => { + findNoteActions().vm.$emit('startReplying'); + + expect(wrapper.emitted('startReplying')).toHaveLength(1); + }); + }); }); |