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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/work_items/components/notes/work_item_note_spec.js')
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js256
1 files changed, 232 insertions, 24 deletions
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([[]]);
+ });
});
});