Commit ef947c73 authored by Savas Vedova's avatar Savas Vedova

Merge branch '228742-use-graphql-to-fetch-notes' into 'master'

Fetch notes along discussions using GraphQL

See merge request gitlab-org/gitlab!69353
parents 1526d0da c67db405
#import "../fragments/note.fragment.graphql"
query vulnerabilityDiscussions( query vulnerabilityDiscussions(
$id: VulnerabilityID! $id: VulnerabilityID!
$after: String $after: String
...@@ -11,6 +13,11 @@ query vulnerabilityDiscussions( ...@@ -11,6 +13,11 @@ query vulnerabilityDiscussions(
nodes { nodes {
id id
replyId replyId
notes {
nodes {
...SecurityDashboardNote
}
}
} }
} }
} }
......
...@@ -9,18 +9,18 @@ import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants'; ...@@ -9,18 +9,18 @@ import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { TYPE_VULNERABILITY } from '~/graphql_shared/constants'; import { TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import { s__, __ } from '~/locale';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { normalizeGraphQLNote } from '../helpers';
import GenericReportSection from './generic_report/report_section.vue'; import GenericReportSection from './generic_report/report_section.vue';
import HistoryEntry from './history_entry.vue'; import HistoryEntry from './history_entry.vue';
import RelatedIssues from './related_issues.vue'; import RelatedIssues from './related_issues.vue';
import RelatedJiraIssues from './related_jira_issues.vue'; import RelatedJiraIssues from './related_jira_issues.vue';
import StatusDescription from './status_description.vue'; import StatusDescription from './status_description.vue';
const TEN_SECONDS = 10000;
export default { export default {
name: 'VulnerabilityFooter', name: 'VulnerabilityFooter',
components: { components: {
...@@ -48,9 +48,9 @@ export default { ...@@ -48,9 +48,9 @@ export default {
}, },
data() { data() {
return { return {
notesLoading: true, discussionsLoading: true,
discussions: [], discussions: [],
lastFetchedAt: null, lastFetchedDiscussionIndex: -1,
}; };
}, },
apollo: { apollo: {
...@@ -60,49 +60,24 @@ export default { ...@@ -60,49 +60,24 @@ export default {
return { id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerability.id) }; return { id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerability.id) };
}, },
update: ({ vulnerability }) => { update: ({ vulnerability }) => {
if (!vulnerability) { return (
return []; vulnerability?.discussions?.nodes.map((discussion) => ({
} ...discussion,
notes: discussion.notes.nodes.map(normalizeGraphQLNote),
return vulnerability.discussions.nodes.map((d) => ({ ...d, notes: [] })); })) || []
);
}, },
result({ error }) { result() {
if (!this.poll && !error) { this.discussionsLoading = false;
this.createNotesPoll(); this.notifyHeaderForStateChangeIfRequired();
this.startPolling();
if (!Visibility.hidden()) {
this.fetchDiscussions();
}
Visibility.change(() => {
if (Visibility.hidden()) {
this.poll.stop();
} else {
this.poll.restart();
}
});
}
}, },
error() { error() {
this.notesLoading = false; this.showGraphQLError();
createFlash({
message: s__(
'VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later.',
),
});
}, },
}, },
}, },
computed: { computed: {
noteDictionary() {
return this.discussions
.flatMap((x) => x.notes)
.reduce((acc, note) => {
acc[note.id] = note;
return acc;
}, {});
},
project() { project() {
return { return {
url: this.vulnerability.project.fullPath, url: this.vulnerability.project.fullPath,
...@@ -137,79 +112,74 @@ export default { ...@@ -137,79 +112,74 @@ export default {
}; };
}, },
}, },
beforeDestroy() {
this.stopPolling();
},
updated() { updated() {
this.$nextTick(() => { this.$nextTick(() => {
initUserPopovers(this.$el.querySelectorAll('.js-user-link')); initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
}); });
}, },
beforeDestroy() {
if (this.poll) {
this.poll.stop();
}
},
methods: { methods: {
fetchDiscussions() { startPolling() {
return this.poll.makeRequest(); if (this.pollInterval) {
return;
}
if (!Visibility.hidden()) {
this.pollInterval = setInterval(this.fetchDiscussions, TEN_SECONDS);
}
this.visibilityListener = Visibility.change(() => {
if (Visibility.hidden()) {
this.stopPolling();
} else {
this.startPolling();
}
});
}, },
findDiscussion(id) { stopPolling() {
return this.discussions.find((d) => d.id === id); if (typeof this.pollInterval !== 'undefined') {
clearInterval(this.pollInterval);
this.pollInterval = undefined;
}
if (typeof this.visibilityListener !== 'undefined') {
Visibility.unbind(this.visibilityListener);
this.visibilityListener = undefined;
}
}, },
createNotesPoll() { showGraphQLError() {
// note: this polling call will be replaced when migrating the vulnerability details page to GraphQL createFlash({
// related epic: https://gitlab.com/groups/gitlab-org/-/epics/3657 message: s__(
this.poll = new Poll({ 'VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later.',
resource: { ),
fetchNotes: () =>
axios.get(this.vulnerability.notesUrl, {
headers: { 'X-Last-Fetched-At': this.lastFetchedAt },
}),
},
method: 'fetchNotes',
successCallback: ({ data: { notes, last_fetched_at: lastFetchedAt } }) => {
this.updateNotes(convertObjectPropsToCamelCase(notes, { deep: true }));
this.lastFetchedAt = lastFetchedAt;
this.notesLoading = false;
},
errorCallback: () => {
this.notesLoading = false;
createFlash({
message: __('Something went wrong while fetching latest comments.'),
});
},
}); });
}, },
updateNotes(notes) { notifyHeaderForStateChangeIfRequired() {
let shallEmitVulnerabilityChangedEvent; const lastItemIndex = this.discussions.length - 1;
notes.forEach((note) => { if (this.lastFetchedDiscussionIndex === lastItemIndex) {
const discussion = this.findDiscussion(note.discussionId); return;
// If the note exists, update it. }
if (this.noteDictionary[note.id]) {
discussion.notes = discussion.notes.map((curr) => (curr.id === note.id ? note : curr));
}
// If the note doesn't exist, but the discussion does, add the note to the discussion.
else if (discussion) {
discussion.notes.push(note);
}
// If the discussion doesn't exist, create it.
else {
this.discussions.push({
id: note.discussionId,
replyId: note.discussionId,
notes: [note],
});
// If the vulnerability status has changed, the note will be a system note.
// Emit an event that tells the header to refresh the vulnerability.
if (note.system === true) {
shallEmitVulnerabilityChangedEvent = true;
}
}
});
if (shallEmitVulnerabilityChangedEvent) { // Do not notify on page load, or first mount.
if (this.lastFetchedDiscussionIndex !== -1) {
this.$emit('vulnerability-state-change'); this.$emit('vulnerability-state-change');
} }
this.lastFetchedDiscussionIndex = lastItemIndex;
},
async fetchDiscussions(callback) {
try {
await this.$apollo.queries.discussions.refetch();
if (typeof callback === 'function') {
callback();
}
} catch {
this.showGraphQLError();
}
}, },
}, },
}; };
...@@ -250,13 +220,14 @@ export default { ...@@ -250,13 +220,14 @@ export default {
</div> </div>
</div> </div>
<hr /> <hr />
<gl-loading-icon v-if="notesLoading" /> <gl-loading-icon v-if="discussionsLoading" />
<ul v-else-if="discussions.length" class="notes discussion-body"> <div v-else-if="discussions.length" class="notes discussion-body">
<history-entry <history-entry
v-for="discussion in discussions" v-for="discussion in discussions"
:key="discussion.id" :key="discussion.id"
:discussion="discussion" :discussion="discussion"
@onCommentUpdated="fetchDiscussions"
/> />
</ul> </div>
</div> </div>
</template> </template>
...@@ -8,7 +8,6 @@ import createFlash from '~/flash'; ...@@ -8,7 +8,6 @@ import createFlash from '~/flash';
import { TYPE_NOTE, TYPE_DISCUSSION, TYPE_VULNERABILITY } from '~/graphql_shared/constants'; import { TYPE_NOTE, TYPE_DISCUSSION, TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { normalizeGraphQLNote } from '../helpers';
import HistoryCommentEditor from './history_comment_editor.vue'; import HistoryCommentEditor from './history_comment_editor.vue';
export default { export default {
...@@ -63,21 +62,13 @@ export default { ...@@ -63,21 +62,13 @@ export default {
]; ];
}, },
initialComment() { initialComment() {
return this.comment?.note; return this.comment?.body;
}, },
canEditComment() { canEditComment() {
return this.comment.currentUser?.canEdit; return this.comment.userPermissions?.adminNote;
}, },
noteHtml() { noteHtml() {
return this.isSavingComment ? undefined : this.comment.noteHtml; return this.isSavingComment ? undefined : this.comment.bodyHtml;
},
},
watch: {
'comment.updatedAt': {
handler() {
this.isSavingComment = false;
},
}, },
}, },
...@@ -95,13 +86,11 @@ export default { ...@@ -95,13 +86,11 @@ export default {
}, },
}); });
const { note, errors } = data.createNote; const { errors } = data.createNote;
if (errors?.length > 0) { if (errors?.length > 0) {
throw errors; throw errors;
} }
this.$emit('onCommentAdded', normalizeGraphQLNote(note));
}, },
async updateComment(body) { async updateComment(body) {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
...@@ -112,14 +101,11 @@ export default { ...@@ -112,14 +101,11 @@ export default {
}, },
}); });
const { note, errors } = data.updateNote; const { errors } = data.updateNote;
if (errors?.length > 0) { if (errors?.length > 0) {
throw errors; throw errors;
} }
this.cancelEditingComment();
this.$emit('onCommentUpdated', normalizeGraphQLNote(note));
}, },
async saveComment(body) { async saveComment(body) {
this.isSavingComment = true; this.isSavingComment = true;
...@@ -131,15 +117,20 @@ export default { ...@@ -131,15 +117,20 @@ export default {
} else { } else {
await this.insertComment(body); await this.insertComment(body);
} }
this.$emit('onCommentUpdated', () => {
this.isSavingComment = false;
this.cancelEditingComment();
});
} catch { } catch {
this.isSavingComment = false;
createFlash({ createFlash({
message: s__( message: s__(
'VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later.', 'VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later.',
), ),
}); });
} }
this.isSavingComment = false;
}, },
async deleteComment() { async deleteComment() {
this.isDeletingComment = true; this.isDeletingComment = true;
...@@ -156,16 +147,18 @@ export default { ...@@ -156,16 +147,18 @@ export default {
throw data.errors; throw data.errors;
} }
this.$emit('onCommentDeleted', this.comment); this.$emit('onCommentUpdated', () => {
this.isDeletingComment = false;
});
} catch { } catch {
this.isDeletingComment = false;
createFlash({ createFlash({
message: s__( message: s__(
'VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later.', 'VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later.',
), ),
}); });
} }
this.isDeletingComment = false;
}, },
cancelEditingComment() { cancelEditingComment() {
this.isEditingComment = false; this.isEditingComment = false;
......
...@@ -3,19 +3,20 @@ import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue' ...@@ -3,19 +3,20 @@ import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue'
import HistoryComment from './history_comment.vue'; import HistoryComment from './history_comment.vue';
export default { export default {
components: { EventItem, HistoryComment }, components: {
EventItem,
HistoryComment,
},
props: { props: {
discussion: { discussion: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
data() {
return {
notes: this.discussion.notes,
};
},
computed: { computed: {
notes() {
return this.discussion.notes;
},
systemNote() { systemNote() {
return this.notes.find((x) => x.system === true); return this.notes.find((x) => x.system === true);
}, },
...@@ -23,35 +24,11 @@ export default { ...@@ -23,35 +24,11 @@ export default {
return this.notes.filter((x) => x !== this.systemNote); return this.notes.filter((x) => x !== this.systemNote);
}, },
}, },
watch: {
discussion(newDiscussion) {
this.notes = newDiscussion.notes;
},
},
methods: {
addComment(note) {
this.notes.push(note);
},
updateComment(note) {
const index = this.notes.findIndex((n) => Number(n.id) === note.id);
if (index > -1) {
this.notes.splice(index, 1, note);
}
},
removeComment(comment) {
const index = this.notes.indexOf(comment);
if (index > -1) {
this.notes.splice(index, 1);
}
},
},
}; };
</script> </script>
<template> <template>
<li v-if="systemNote" class="card border-bottom system-note p-0"> <div v-if="systemNote" class="card border-bottom system-note p-0">
<event-item <event-item
:id="systemNote.id" :id="systemNote.id"
:author="systemNote.author" :author="systemNote.author"
...@@ -60,27 +37,22 @@ export default { ...@@ -60,27 +37,22 @@ export default {
icon-class="timeline-icon m-0" icon-class="timeline-icon m-0"
class="m-3" class="m-3"
> >
<template #header-message>{{ systemNote.note }}</template> <template #header-message>{{ systemNote.body }}</template>
</event-item> </event-item>
<hr v-if="comments.length" class="gl-m-0" />
<template v-if="comments.length" ref="existingComments"> <history-comment
<hr class="m-3" /> v-for="comment in comments"
<history-comment ref="existingComment"
v-for="comment in comments" :key="comment.id"
:key="comment.id" :comment="comment"
ref="existingComment" :discussion-id="discussion.replyId"
:comment="comment" v-on="$listeners"
:discussion-id="discussion.replyId" />
@onCommentUpdated="updateComment"
@onCommentDeleted="removeComment"
/>
</template>
<history-comment <history-comment
v-else v-if="!comments.length"
ref="newComment" ref="newComment"
:discussion-id="discussion.replyId" :discussion-id="discussion.replyId"
@onCommentAdded="addComment" v-on="$listeners"
/> />
</li> </div>
</template> </template>
...@@ -37,11 +37,6 @@ export const normalizeGraphQLNote = (note) => { ...@@ -37,11 +37,6 @@ export const normalizeGraphQLNote = (note) => {
return { return {
...note, ...note,
id: getIdFromGraphQLId(note.id), id: getIdFromGraphQLId(note.id),
note: note.body,
noteHtml: note.bodyHtml,
currentUser: {
canEdit: note.userPermissions?.adminNote,
},
author: { author: {
...note.author, ...note.author,
id: getIdFromGraphQLId(note.author.id), id: getIdFromGraphQLId(note.author.id),
......
...@@ -12,6 +12,7 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -12,6 +12,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { TYPE_DISCUSSION, TYPE_VULNERABILITY } from '~/graphql_shared/constants'; import { TYPE_DISCUSSION, TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { generateNote } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -59,28 +60,7 @@ describe('History Comment', () => { ...@@ -59,28 +60,7 @@ describe('History Comment', () => {
}); });
}; };
const note = { const note = generateNote();
id: 'gid://gitlab/DiscussionNote/1295',
body: 'Created a note.',
bodyHtml: '\u003cp\u003eCreated a note\u003c/p\u003e',
updatedAt: '2021-08-25T16:21:18Z',
system: false,
systemNoteIconName: null,
userPermissions: {
adminNote: true,
},
author: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
webPath: '/root',
},
};
// Needed for now. Will be removed when fetching notes will be done through GraphQL.
note.note = note.body;
note.noteHtml = note.bodyHtml;
note.currentUser = { canEdit: note.userPermissions.adminNote };
beforeEach(() => { beforeEach(() => {
createNoteMutationSpy = jest createNoteMutationSpy = jest
...@@ -95,8 +75,8 @@ describe('History Comment', () => { ...@@ -95,8 +75,8 @@ describe('History Comment', () => {
}); });
const addCommentButton = () => wrapper.find({ ref: 'addCommentButton' }); const addCommentButton = () => wrapper.find({ ref: 'addCommentButton' });
const commentEditor = () => wrapper.find(HistoryCommentEditor); const commentEditor = () => wrapper.findComponent(HistoryCommentEditor);
const eventItem = () => wrapper.find(EventItem); const eventItem = () => wrapper.findComponent(EventItem);
const editButton = () => wrapper.find('[title="Edit Comment"]'); const editButton = () => wrapper.find('[title="Edit Comment"]');
const deleteButton = () => wrapper.find('[title="Delete Comment"]'); const deleteButton = () => wrapper.find('[title="Delete Comment"]');
const confirmDeleteButton = () => wrapper.find({ ref: 'confirmDeleteButton' }); const confirmDeleteButton = () => wrapper.find({ ref: 'confirmDeleteButton' });
...@@ -228,10 +208,10 @@ describe('History Comment', () => { ...@@ -228,10 +208,10 @@ describe('History Comment', () => {
}; };
describe.each` describe.each`
desc | propsData | expectedEvent | expectedVars | mutationSpyFn | queryName desc | propsData | expectedVars | mutationSpyFn | queryName
${'inserting a new note'} | ${{}} | ${'onCommentAdded'} | ${EXPECTED_CREATE_VARS} | ${() => createNoteMutationSpy} | ${CREATE_NOTE} ${'inserting a new note'} | ${{}} | ${EXPECTED_CREATE_VARS} | ${() => createNoteMutationSpy} | ${CREATE_NOTE}
${'updating an existing note'} | ${{ comment: note }} | ${'onCommentUpdated'} | ${EXPECTED_UPDATE_VARS} | ${() => updateNoteMutationSpy} | ${UPDATE_NOTE} ${'updating an existing note'} | ${{ comment: note }} | ${EXPECTED_UPDATE_VARS} | ${() => updateNoteMutationSpy} | ${UPDATE_NOTE}
`('$desc', ({ propsData, expectedEvent, expectedVars, mutationSpyFn, queryName }) => { `('$desc', ({ propsData, expectedVars, mutationSpyFn, queryName }) => {
let mutationSpy; let mutationSpy;
beforeEach(() => { beforeEach(() => {
...@@ -258,25 +238,19 @@ describe('History Comment', () => { ...@@ -258,25 +238,19 @@ describe('History Comment', () => {
expect(commentEditor().props('isSaving')).toBe(true); expect(commentEditor().props('isSaving')).toBe(true);
}); });
it('emits event when mutation is successful', async () => { it('emits event when mutation is successful with a callback function that resets the state', async () => {
createWrapper({ propsData }); createWrapper({ propsData });
const listener = jest.fn().mockImplementation((callback) => callback());
wrapper.vm.$on('onCommentUpdated', listener);
await editAndSaveNewContent('new comment'); await editAndSaveNewContent('new comment');
expect(commentEditor().props('isSaving')).toBe(true);
await waitForPromises(); await waitForPromises();
expect(wrapper.emitted(expectedEvent)).toEqual([ expect(wrapper.emitted('onCommentUpdated')).toEqual([[expect.any(Function)]]);
[ expect(listener).toHaveBeenCalled();
{ expect(commentEditor().exists()).toBe(false);
...note,
id: 1295,
author: {
...note.author,
id: 1,
path: note.author.webPath,
},
},
],
]);
}); });
describe('when mutation has data error', () => { describe('when mutation has data error', () => {
...@@ -316,7 +290,7 @@ describe('History Comment', () => { ...@@ -316,7 +290,7 @@ describe('History Comment', () => {
}); });
describe('deleting a note', () => { describe('deleting a note', () => {
it('deletes the comment when the confirm delete button is clicked', async () => { it('deletes the comment when the confirm delete button is clicked and submits an event to refect the discussions', async () => {
createWrapper({ createWrapper({
propsData: { comment: note }, propsData: { comment: note },
}); });
...@@ -331,8 +305,8 @@ describe('History Comment', () => { ...@@ -331,8 +305,8 @@ describe('History Comment', () => {
expect(cancelDeleteButton().props('disabled')).toBe(true); expect(cancelDeleteButton().props('disabled')).toBe(true);
await waitForPromises(); await waitForPromises();
expect(wrapper.emitted().onCommentDeleted).toBeTruthy(); expect(wrapper.emitted().onCommentUpdated).toBeTruthy();
expect(wrapper.emitted().onCommentDeleted[0][0]).toEqual(note); expect(wrapper.emitted().onCommentUpdated[0][0]).toEqual(expect.any(Function));
}); });
it('sends mutation to delete note', async () => { it('sends mutation to delete note', async () => {
...@@ -383,7 +357,7 @@ describe('History Comment', () => { ...@@ -383,7 +357,7 @@ describe('History Comment', () => {
it('does not show the edit/delete buttons if the current user has no edit permissions', () => { it('does not show the edit/delete buttons if the current user has no edit permissions', () => {
createWrapper({ createWrapper({
propsData: { propsData: {
comment: { ...note, userPermissions: undefined, currentUser: { canEdit: false } }, comment: { ...note, userPermissions: { adminNote: false } },
}, },
}); });
......
...@@ -8,7 +8,7 @@ describe('History Entry', () => { ...@@ -8,7 +8,7 @@ describe('History Entry', () => {
const systemNote = { const systemNote = {
system: true, system: true,
id: 1, id: 1,
note: 'changed vulnerability status to dismissed', body: 'changed vulnerability status to dismissed',
systemNoteIconName: 'cancel', systemNoteIconName: 'cancel',
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
author: { author: {
...@@ -20,11 +20,8 @@ describe('History Entry', () => { ...@@ -20,11 +20,8 @@ describe('History Entry', () => {
const commentNote = { const commentNote = {
id: 2, id: 2,
note: 'some note', body: 'some note',
author: {}, author: {},
currentUser: {
canEdit: true,
},
}; };
const createWrapper = (...notes) => { const createWrapper = (...notes) => {
...@@ -33,7 +30,6 @@ describe('History Entry', () => { ...@@ -33,7 +30,6 @@ describe('History Entry', () => {
wrapper = shallowMount(HistoryEntry, { wrapper = shallowMount(HistoryEntry, {
propsData: { propsData: {
discussion, discussion,
notesUrl: '/notes',
}, },
stubs: { EventItem }, stubs: { EventItem },
}); });
...@@ -49,7 +45,7 @@ describe('History Entry', () => { ...@@ -49,7 +45,7 @@ describe('History Entry', () => {
it('passes the expected values to the event item component', () => { it('passes the expected values to the event item component', () => {
createWrapper(systemNote); createWrapper(systemNote);
expect(eventItem().text()).toContain(systemNote.note); expect(eventItem().text()).toContain(systemNote.body);
expect(eventItem().props()).toMatchObject({ expect(eventItem().props()).toMatchObject({
id: systemNote.id, id: systemNote.id,
author: systemNote.author, author: systemNote.author,
...@@ -80,33 +76,4 @@ describe('History Entry', () => { ...@@ -80,33 +76,4 @@ describe('History Entry', () => {
expect(commentAt(0).props('comment')).toEqual(commentNote); expect(commentAt(0).props('comment')).toEqual(commentNote);
expect(commentAt(1).props('comment')).toEqual(commentNoteClone); expect(commentAt(1).props('comment')).toEqual(commentNoteClone);
}); });
it('adds a new comment correctly', async () => {
createWrapper(systemNote);
newComment().vm.$emit('onCommentAdded', commentNote);
await wrapper.vm.$nextTick();
expect(newComment().exists()).toBe(false);
expect(existingComments()).toHaveLength(1);
expect(commentAt(0).props('comment')).toEqual(commentNote);
});
it('updates an existing comment correctly', async () => {
const updatedNote = { ...commentNote, note: 'new note' };
createWrapper(systemNote, commentNote);
commentAt(0).vm.$emit('onCommentUpdated', updatedNote);
await wrapper.vm.$nextTick();
expect(commentAt(0).props('comment')).toBe(updatedNote);
});
it('deletes an existing comment correctly', async () => {
createWrapper(systemNote, commentNote);
await commentAt(0).vm.$emit('onCommentDeleted', commentNote);
expect(newComment().exists()).toBe(true);
expect(existingComments()).toHaveLength(0);
});
}); });
export const generateNote = ({ id = 1295 } = {}) => ({
id: `gid://gitlab/DiscussionNote/${id}`,
body: 'Created a note.',
bodyHtml: '\u003cp\u003eCreated a note\u003c/p\u003e',
updatedAt: '2021-08-25T16:21:18Z',
system: false,
systemNoteIconName: null,
userPermissions: {
adminNote: true,
},
author: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
webPath: '/root',
},
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment