Commit f97400aa authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '228743-use-graphql-to-save-a-history-note' into 'master'

Save a comment through graphql

See merge request gitlab-org/gitlab!68995
parents fea18c21 5fdb14b1
......@@ -14,3 +14,4 @@ export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
export const TYPE_NOTE = 'Note';
export const TYPE_DISCUSSION = 'Discussion';
fragment SecurityDashboardNote on Note {
id
system
body
bodyHtml
updatedAt
systemNoteIconName
userPermissions {
adminNote
}
author {
id
name
webPath
username
}
}
#import "../fragments/note.fragment.graphql"
mutation securityDashboardCreateNote(
$noteableId: NoteableID!
$discussionId: DiscussionID
$confidential: Boolean
$body: String!
) {
createNote(
input: {
noteableId: $noteableId
body: $body
confidential: $confidential
discussionId: $discussionId
}
) {
errors
note {
...SecurityDashboardNote
}
}
}
#import "../fragments/note.fragment.graphql"
mutation securityDashboardUpdateNote($id: NoteID!, $confidential: Boolean, $body: String!) {
updateNote(input: { id: $id, body: $body, confidential: $confidential }) {
errors
note {
...SecurityDashboardNote
}
}
}
......@@ -71,7 +71,7 @@ export default {
this.createNotesPoll();
if (!Visibility.hidden()) {
this.poll.makeRequest();
this.fetchDiscussions();
}
Visibility.change(() => {
......@@ -148,6 +148,9 @@ export default {
}
},
methods: {
fetchDiscussions() {
return this.poll.makeRequest();
},
findDiscussion(id) {
return this.discussions.find((d) => d.id === id);
},
......@@ -253,7 +256,6 @@ export default {
v-for="discussion in discussions"
:key="discussion.id"
:discussion="discussion"
:notes-url="vulnerability.notesUrl"
/>
</ul>
</div>
......
<script>
import { GlButton, GlSafeHtmlDirective as SafeHtml, GlLoadingIcon } from '@gitlab/ui';
import deleteNoteMutation from 'ee/security_dashboard/graphql/mutations/note_delete.mutation.graphql';
import createNoteMutation from 'ee/security_dashboard/graphql/mutations/note_create.mutation.graphql';
import destroyNoteMutation from 'ee/security_dashboard/graphql/mutations/note_destroy.mutation.graphql';
import updateNoteMutation from 'ee/security_dashboard/graphql/mutations/note_update.mutation.graphql';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import createFlash from '~/flash';
import { TYPE_NOTE } from '~/graphql_shared/constants';
import { TYPE_NOTE, TYPE_DISCUSSION, TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
import { normalizeGraphQLNote } from '../helpers';
import HistoryCommentEditor from './history_comment_editor.vue';
export default {
......@@ -21,6 +23,8 @@ export default {
SafeHtml,
},
inject: ['vulnerabilityId'],
props: {
comment: {
type: Object,
......@@ -32,10 +36,6 @@ export default {
required: false,
default: undefined,
},
notesUrl: {
type: String,
required: true,
},
},
data() {
......@@ -63,7 +63,7 @@ export default {
];
},
initialComment() {
return this.comment && this.comment.note;
return this.comment?.note;
},
canEditComment() {
return this.comment.currentUser?.canEdit;
......@@ -85,51 +85,79 @@ export default {
showCommentInput() {
this.isEditingComment = true;
},
getSaveConfig(note) {
const isUpdatingComment = Boolean(this.comment);
const method = isUpdatingComment ? 'put' : 'post';
const url = isUpdatingComment ? this.comment.path : this.notesUrl;
const data = { note: { note } };
const emitName = isUpdatingComment ? 'onCommentUpdated' : 'onCommentAdded';
// If we're saving a new comment, use the discussion ID in the request data.
if (!isUpdatingComment) {
data.in_reply_to_discussion_id = this.discussionId;
async insertComment(body) {
const { data } = await this.$apollo.mutate({
mutation: createNoteMutation,
variables: {
noteableId: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerabilityId),
discussionId: convertToGraphQLId(TYPE_DISCUSSION, this.discussionId),
body,
},
});
const { note, errors } = data.createNote;
if (errors?.length > 0) {
throw errors;
}
return { method, url, data, emitName };
this.$emit('onCommentAdded', normalizeGraphQLNote(note));
},
saveComment(note) {
async updateComment(body) {
const { data } = await this.$apollo.mutate({
mutation: updateNoteMutation,
variables: {
id: convertToGraphQLId(TYPE_NOTE, this.comment.id),
body,
},
});
const { note, errors } = data.updateNote;
if (errors?.length > 0) {
throw errors;
}
this.cancelEditingComment();
this.$emit('onCommentUpdated', normalizeGraphQLNote(note));
},
async saveComment(body) {
this.isSavingComment = true;
const { method, url, data, emitName } = this.getSaveConfig(note);
// note: this direct API call will be replaced when migrating the vulnerability details page to GraphQL
// related epic: https://gitlab.com/groups/gitlab-org/-/epics/3657
axios({ method, url, data })
.then(({ data: responseData }) => {
this.isEditingComment = false;
this.$emit(emitName, { response: responseData, comment: this.comment });
})
.catch(() => {
createFlash({
message: s__(
'VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later.',
),
});
const isUpdatingComment = Boolean(this.comment);
try {
if (isUpdatingComment) {
await this.updateComment(body);
} else {
await this.insertComment(body);
}
} catch {
createFlash({
message: s__(
'VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later.',
),
});
}
this.isSavingComment = false;
},
async deleteComment() {
this.isDeletingComment = true;
try {
await this.$apollo.mutate({
mutation: deleteNoteMutation,
const { data } = await this.$apollo.mutate({
mutation: destroyNoteMutation,
variables: {
id: convertToGraphQLId(TYPE_NOTE, this.comment.id),
},
});
if (data.errors?.length > 0) {
throw data.errors;
}
this.$emit('onCommentDeleted', this.comment);
} catch (e) {
} catch {
createFlash({
message: s__(
'VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later.',
......
<script>
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import HistoryComment from './history_comment.vue';
export default {
......@@ -10,10 +9,6 @@ export default {
type: Object,
required: true,
},
notesUrl: {
type: String,
required: true,
},
},
data() {
return {
......@@ -34,14 +29,14 @@ export default {
},
},
methods: {
addComment({ response }) {
this.notes.push(convertObjectPropsToCamelCase(response));
addComment(note) {
this.notes.push(note);
},
updateComment({ response, comment }) {
const index = this.notes.indexOf(comment);
updateComment(note) {
const index = this.notes.findIndex((n) => Number(n.id) === note.id);
if (index > -1) {
this.notes.splice(index, 1, { ...comment, ...convertObjectPropsToCamelCase(response) });
this.notes.splice(index, 1, note);
}
},
removeComment(comment) {
......@@ -76,7 +71,6 @@ export default {
ref="existingComment"
:comment="comment"
:discussion-id="discussion.replyId"
:notes-url="notesUrl"
@onCommentUpdated="updateComment"
@onCommentDeleted="removeComment"
/>
......@@ -86,7 +80,6 @@ export default {
v-else
ref="newComment"
:discussion-id="discussion.replyId"
:notes-url="notesUrl"
@onCommentAdded="addComment"
/>
</li>
......
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isAbsolute, isSafeURL } from '~/lib/utils/url_utility';
import { REGEXES, gidPrefix, uidPrefix } from './constants';
......@@ -28,6 +29,27 @@ export const getAddRelatedIssueRequestParams = (reference, defaultProjectId) =>
return { target_issue_iid: issueId, target_project_id: projectId };
};
export const normalizeGraphQLNote = (note) => {
if (!note) {
return null;
}
return {
...note,
id: getIdFromGraphQLId(note.id),
note: note.body,
noteHtml: note.bodyHtml,
currentUser: {
canEdit: note.userPermissions?.adminNote,
},
author: {
...note.author,
id: getIdFromGraphQLId(note.author.id),
path: note.author.webPath,
},
};
};
export const normalizeGraphQLVulnerability = (vulnerability) => {
if (!vulnerability) {
return null;
......
......@@ -141,12 +141,10 @@ describe('Vulnerability Footer', () => {
expect(findDiscussions().at(0).props()).toEqual({
discussion: { ...discussion1, notes: [convertObjectPropsToCamelCase(notes[0])] },
notesUrl: vulnerability.notesUrl,
});
expect(findDiscussions().at(1).props()).toEqual({
discussion: { ...discussion2, notes: [convertObjectPropsToCamelCase(notes[1])] },
notesUrl: vulnerability.notesUrl,
});
});
......
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import deleteNoteMutation from 'ee/security_dashboard/graphql/mutations/note_delete.mutation.graphql';
import createNoteMutation from 'ee/security_dashboard/graphql/mutations/note_create.mutation.graphql';
import destroyNoteMutation from 'ee/security_dashboard/graphql/mutations/note_destroy.mutation.graphql';
import updateNoteMutation from 'ee/security_dashboard/graphql/mutations/note_update.mutation.graphql';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import HistoryComment from 'ee/vulnerabilities/components/history_comment.vue';
import HistoryCommentEditor from 'ee/vulnerabilities/components/history_comment_editor.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { TYPE_DISCUSSION, TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
const mockAxios = new MockAdapter(axios);
jest.mock('~/flash');
Vue.use(VueApollo);
const CREATE_NOTE = 'createNote';
const UPDATE_NOTE = 'updateNote';
const DESTROY_NOTE = 'destroyNote';
const TEST_VULNERABILITY_ID = '15';
const TEST_DISCUSSION_ID = '24';
const TEST_VULNERABILITY_GID = convertToGraphQLId(TYPE_VULNERABILITY, TEST_VULNERABILITY_ID);
const TEST_DISCUSSION_GID = convertToGraphQLId(TYPE_DISCUSSION, TEST_DISCUSSION_ID);
describe('History Comment', () => {
let wrapper;
let createNoteMutationSpy;
let updateNoteMutationSpy;
let destroyNoteMutationSpy;
const createMutationResponse = ({ note = {}, queryName, errors = [] }) => ({
data: {
[queryName]: {
errors,
note,
},
},
});
const createApolloProvider = (...queries) => {
return createMockApollo([...queries]);
};
const createWrapper = ({ propsData } = {}) => {
const apolloProvider = createMockApollo([
[createNoteMutation, createNoteMutationSpy],
[updateNoteMutation, updateNoteMutationSpy],
[destroyNoteMutation, destroyNoteMutationSpy],
]);
const createWrapper = ({ comment, apolloProvider } = {}) => {
wrapper = mount(HistoryComment, {
apolloProvider,
provide: {
vulnerabilityId: TEST_VULNERABILITY_ID,
},
propsData: {
comment,
notesUrl: '/notes',
discussionId: TEST_DISCUSSION_ID,
...propsData,
},
});
};
const comment = {
id: 'id',
note: 'note',
noteHtml: '<p>note</p>',
author: {},
updatedAt: new Date().toISOString(),
currentUser: {
canEdit: true,
const note = {
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(() => {
createNoteMutationSpy = jest
.fn()
.mockResolvedValue(createMutationResponse({ queryName: CREATE_NOTE, note }));
destroyNoteMutationSpy = jest
.fn()
.mockResolvedValue(createMutationResponse({ queryName: DESTROY_NOTE, note: null }));
updateNoteMutationSpy = jest
.fn()
.mockResolvedValue(createMutationResponse({ queryName: UPDATE_NOTE, note }));
});
const addCommentButton = () => wrapper.find({ ref: 'addCommentButton' });
const commentEditor = () => wrapper.find(HistoryCommentEditor);
const eventItem = () => wrapper.find(EventItem);
......@@ -83,9 +134,13 @@ describe('History Comment', () => {
return wrapper.vm.$nextTick();
};
const editAndSaveNewContent = async (content) => {
await showEditView();
commentEditor().vm.$emit('onSave', content);
};
afterEach(() => {
wrapper.destroy();
mockAxios.reset();
createFlash.mockReset();
});
......@@ -111,57 +166,22 @@ describe('History Comment', () => {
})
.then(expectAddCommentView);
});
it('saves the comment when the save button is clicked on the comment editor', () => {
mockAxios.onPost().replyOnce(200, comment);
return showEditView()
.then(() => {
commentEditor().vm.$emit('onSave', 'new comment');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(commentEditor().props('isSaving')).toBe(true);
return axios.waitForAll();
})
.then(() => {
expect(mockAxios.history.post).toHaveLength(1);
expect(wrapper.emitted().onCommentAdded[0]).toEqual([
{ response: comment, comment: undefined },
]);
});
});
it('shows an error message and continues showing the comment editor when the comment cannot be saved', () => {
mockAxios.onPost().replyOnce(500);
return showEditView()
.then(() => {
commentEditor().vm.$emit('onSave', 'new comment');
return axios.waitForAll();
})
.then(() => {
expect(mockAxios.history.post).toHaveLength(1);
expect(createFlash).toHaveBeenCalledTimes(1);
expect(commentEditor().exists()).toBe(true);
});
});
});
describe(`when there's an existing comment`, () => {
beforeEach(() => createWrapper({ comment }));
beforeEach(() => createWrapper({ propsData: { comment: note, discussionId: '24' } }));
it('shows the comment with the correct user author and timestamp and the edit/delete buttons', () => {
expectExistingCommentView();
expect(eventItem().props('author')).toBe(comment.author);
expect(eventItem().props('createdAt')).toBe(comment.updatedAt);
expect(eventItem().element.innerHTML).toContain(comment.noteHtml);
expect(eventItem().props('author')).toBe(note.author);
expect(eventItem().props('createdAt')).toBe(note.updatedAt);
expect(eventItem().element.innerHTML).toContain(note.bodyHtml);
});
it('shows the comment editor when the edit button is clicked', () => {
return showEditView().then(() => {
expectEditCommentView();
expect(commentEditor().props('initialComment')).toBe(comment.note);
expect(commentEditor().props('initialComment')).toBe(note.body);
});
});
......@@ -173,7 +193,7 @@ describe('History Comment', () => {
})
.then(() => {
expectExistingCommentView();
expect(eventItem().element.innerHTML).toContain(comment.noteHtml);
expect(eventItem().element.innerHTML).toContain(note.bodyHtml);
});
});
......@@ -194,61 +214,111 @@ describe('History Comment', () => {
})
.then(() => {
expectExistingCommentView();
expect(eventItem().element.innerHTML).toContain(comment.noteHtml);
expect(eventItem().element.innerHTML).toContain(note.bodyHtml);
});
});
});
it('saves the comment when the save button is clicked on the comment editor', () => {
const responseData = { ...comment, note: 'new comment' };
mockAxios.onPut().replyOnce(200, responseData);
const EXPECTED_CREATE_VARS = {
discussionId: TEST_DISCUSSION_GID,
noteableId: TEST_VULNERABILITY_GID,
};
const EXPECTED_UPDATE_VARS = {
id: note.id,
};
return showEditView()
.then(() => {
commentEditor().vm.$emit('onSave', responseData.note);
return wrapper.vm.$nextTick();
})
.then(() => {
expect(commentEditor().props('isSaving')).toBe(true);
return axios.waitForAll();
})
.then(() => {
expect(mockAxios.history.put).toHaveLength(1);
expect(wrapper.emitted().onCommentUpdated[0]).toEqual([
{ response: responseData, comment },
]);
describe.each`
desc | propsData | expectedEvent | expectedVars | mutationSpyFn | queryName
${'inserting a new note'} | ${{}} | ${'onCommentAdded'} | ${EXPECTED_CREATE_VARS} | ${() => createNoteMutationSpy} | ${CREATE_NOTE}
${'updating an existing note'} | ${{ comment: note }} | ${'onCommentUpdated'} | ${EXPECTED_UPDATE_VARS} | ${() => updateNoteMutationSpy} | ${UPDATE_NOTE}
`('$desc', ({ propsData, expectedEvent, expectedVars, mutationSpyFn, queryName }) => {
let mutationSpy;
beforeEach(() => {
mutationSpy = mutationSpyFn();
});
it('sends graphql mutation', async () => {
createWrapper({ propsData });
await editAndSaveNewContent('new comment');
expect(mutationSpy).toHaveBeenCalledWith({
...expectedVars,
body: 'new comment',
});
});
it('shows loading', async () => {
createWrapper({ propsData });
await editAndSaveNewContent('new comment');
await wrapper.vm.$nextTick();
expect(commentEditor().props('isSaving')).toBe(true);
});
it('emits event when mutation is successful', async () => {
createWrapper({ propsData });
await editAndSaveNewContent('new comment');
await waitForPromises();
expect(wrapper.emitted(expectedEvent)).toEqual([
[
{
...note,
id: 1295,
author: {
...note.author,
id: 1,
path: note.author.webPath,
},
},
],
]);
});
describe('when mutation has data error', () => {
beforeEach(() => {
mutationSpy.mockResolvedValue({ queryName, errors: ['Some domain specific error'] });
createWrapper({ propsData });
});
it('shows flash', async () => {
await editAndSaveNewContent('new comment');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while trying to save the comment. Please try again later.',
});
});
});
it('shows an error message when the comment cannot be saved', () => {
mockAxios.onPut().replyOnce(500);
describe('when mutation has top-level error', () => {
beforeEach(() => {
mutationSpy.mockRejectedValue(new Error('Something top-level happened'));
return showEditView()
.then(() => {
commentEditor().vm.$emit('onSave', 'some comment');
return axios.waitForAll();
})
.then(() => {
expect(mockAxios.history.put).toHaveLength(1);
expect(createFlash).toHaveBeenCalledTimes(1);
createWrapper({ propsData });
});
it('shows flash', async () => {
await editAndSaveNewContent('new comment');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while trying to save the comment. Please try again later.',
});
expect(commentEditor().exists()).toBe(true);
});
});
});
describe('deleting a note', () => {
it('deletes the comment when the confirm delete button is clicked', async () => {
createWrapper({
comment,
apolloProvider: createApolloProvider([
deleteNoteMutation,
jest.fn().mockResolvedValue({
data: {
destroyNote: {
errors: [],
note: null,
},
},
}),
]),
propsData: { comment: note },
});
deleteButton().trigger('click');
......@@ -260,40 +330,62 @@ describe('History Comment', () => {
expect(confirmDeleteButton().props('loading')).toBe(true);
expect(cancelDeleteButton().props('disabled')).toBe(true);
await axios.waitForAll();
await waitForPromises();
expect(wrapper.emitted().onCommentDeleted).toBeTruthy();
expect(wrapper.emitted().onCommentDeleted[0][0]).toEqual(comment);
expect(wrapper.emitted().onCommentDeleted[0][0]).toEqual(note);
});
it('shows an error message when the comment cannot be deleted', async () => {
createWrapper({
comment,
apolloProvider: createApolloProvider([
deleteNoteMutation,
jest.fn().mockRejectedValue({
data: {
destroyNote: {
errors: [{ message: 'Something went wrong' }],
note: null,
},
},
}),
]),
it('sends mutation to delete note', async () => {
createWrapper({ propsData: { comment: note } });
deleteButton().trigger('click');
await wrapper.vm.$nextTick();
confirmDeleteButton().trigger('click');
expect(destroyNoteMutationSpy).toHaveBeenCalledWith({
id: note.id,
});
});
it('with data error, shows an error message', async () => {
destroyNoteMutationSpy.mockResolvedValue({ errors: ['Some domain specific error'] });
createWrapper({ propsData: { comment: note } });
deleteButton().trigger('click');
await wrapper.vm.$nextTick();
confirmDeleteButton().trigger('click');
await axios.waitForAll();
expect(createFlash).toHaveBeenCalledTimes(1);
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while trying to delete the comment. Please try again later.',
});
});
it('with top-level error, shows an error message', async () => {
destroyNoteMutationSpy.mockRejectedValue(new Error('Some top-level error'));
createWrapper({ propsData: { comment: note } });
deleteButton().trigger('click');
await wrapper.vm.$nextTick();
confirmDeleteButton().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while trying to delete the comment. Please try again later.',
});
});
});
describe('no permission to edit existing comment', () => {
it('does not show the edit/delete buttons if the current user has no edit permissions', () => {
createWrapper({ comment: { ...comment, currentUser: { canEdit: false } } });
createWrapper({
propsData: {
comment: { ...note, userPermissions: undefined, currentUser: { canEdit: false } },
},
});
expect(editButton().exists()).toBe(false);
expect(deleteButton().exists()).toBe(false);
......
import { shallowMount } from '@vue/test-utils';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import HistoryEntry from 'ee/vulnerabilities/components/history_entry.vue';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
describe('History Entry', () => {
let wrapper;
......@@ -84,9 +83,7 @@ describe('History Entry', () => {
it('adds a new comment correctly', async () => {
createWrapper(systemNote);
newComment().vm.$emit('onCommentAdded', {
response: convertObjectPropsToSnakeCase(commentNote),
});
newComment().vm.$emit('onCommentAdded', commentNote);
await wrapper.vm.$nextTick();
......@@ -96,13 +93,13 @@ describe('History Entry', () => {
});
it('updates an existing comment correctly', async () => {
const response = { note: 'new note' };
const updatedNote = { ...commentNote, note: 'new note' };
createWrapper(systemNote, commentNote);
commentAt(0).vm.$emit('onCommentUpdated', { response, comment: commentNote });
commentAt(0).vm.$emit('onCommentUpdated', updatedNote);
await wrapper.vm.$nextTick();
expect(commentAt(0).props('comment')).toEqual({ ...commentNote, note: response.note });
expect(commentAt(0).props('comment')).toBe(updatedNote);
});
it('deletes an existing comment correctly', async () => {
......
......@@ -4,6 +4,7 @@ import Footer from 'ee/vulnerabilities/components/footer.vue';
import Header from 'ee/vulnerabilities/components/header.vue';
import Main from 'ee/vulnerabilities/components/vulnerability.vue';
import Details from 'ee/vulnerabilities/components/vulnerability_details.vue';
import { stubComponent } from 'helpers/stub_component';
const mockAxios = new AxiosMockAdapter();
......@@ -47,6 +48,10 @@ describe('Vulnerability', () => {
propsData: {
vulnerability,
},
stubs: {
VulnerabilityHeader: stubComponent(Header),
VulnerabilityFooter: stubComponent(Footer),
},
});
};
......@@ -77,28 +82,25 @@ describe('Vulnerability', () => {
});
describe('vulnerability state change event', () => {
let fetchDiscussions;
let makeRequest;
let refreshVulnerability;
beforeEach(() => {
fetchDiscussions = jest.fn();
refreshVulnerability = jest.fn();
findHeader().vm.refreshVulnerability = refreshVulnerability;
findFooter().vm.fetchDiscussions = fetchDiscussions;
refreshVulnerability = jest.spyOn(findHeader().vm, 'refreshVulnerability');
makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions');
});
it('updates the footer notes when the vulnerbility state was changed', () => {
findHeader().vm.$emit('vulnerability-state-change');
expect(fetchDiscussions).toHaveBeenCalledTimes(1);
expect(makeRequest).toHaveBeenCalledTimes(1);
expect(refreshVulnerability).not.toHaveBeenCalled();
});
it('updates the header when the footer received a state-change note', () => {
findFooter().vm.$emit('vulnerability-state-change');
expect(fetchDiscussions).not.toHaveBeenCalled();
expect(makeRequest).not.toHaveBeenCalled();
expect(refreshVulnerability).toHaveBeenCalledTimes(1);
});
});
......
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