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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/admin/abuse_report/components/notes')
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js227
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js214
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js72
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_edit_note_spec.js129
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js79
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js126
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);
+ });
+ });
});