diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/queries/vulnerability_discussions.query.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/queries/vulnerability_discussions.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..7588d96f4d23b86f972a0476fb19a24d861bd032 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/graphql/queries/vulnerability_discussions.query.graphql @@ -0,0 +1,17 @@ +query vulnerabilityDiscussions( + $id: VulnerabilityID! + $after: String + $before: String + $first: Int + $last: Int +) { + vulnerability(id: $id) { + id + discussions(after: $after, before: $before, first: $first, last: $last) { + nodes { + id + replyId + } + } + } +} diff --git a/ee/app/assets/javascripts/vulnerabilities/components/footer.vue b/ee/app/assets/javascripts/vulnerabilities/components/footer.vue index 14dcc0205aeba0068ae8d062830865c0fc0e049c..e6b9cfe8d4195c785682d4708efc0f4fa5d0c7e6 100644 --- a/ee/app/assets/javascripts/vulnerabilities/components/footer.vue +++ b/ee/app/assets/javascripts/vulnerabilities/components/footer.vue @@ -1,11 +1,14 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; import Visibility from 'visibilityjs'; import Api from 'ee/api'; +import vulnerabilityDiscussionsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_discussions.query.graphql'; import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue'; import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue'; import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants'; import createFlash from '~/flash'; +import { TYPE_VULNERABILITY } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import axios from '~/lib/utils/axios_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import Poll from '~/lib/utils/poll'; @@ -27,6 +30,7 @@ export default { HistoryEntry, RelatedIssues, RelatedJiraIssues, + GlLoadingIcon, GlIcon, StatusDescription, }, @@ -44,14 +48,53 @@ export default { }, data() { return { - discussionsDictionary: {}, + notesLoading: true, + discussions: [], lastFetchedAt: null, }; }, - computed: { - discussions() { - return Object.values(this.discussionsDictionary); + apollo: { + discussions: { + query: vulnerabilityDiscussionsQuery, + variables() { + return { id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerability.id) }; + }, + update: ({ vulnerability }) => { + if (!vulnerability) { + return []; + } + + return vulnerability.discussions.nodes.map((d) => ({ ...d, notes: [] })); + }, + result({ error }) { + if (!this.poll && !error) { + this.createNotesPoll(); + + if (!Visibility.hidden()) { + this.poll.makeRequest(); + } + + Visibility.change(() => { + if (Visibility.hidden()) { + this.poll.stop(); + } else { + this.poll.restart(); + } + }); + } + }, + error() { + this.notesLoading = false; + + createFlash({ + message: s__( + 'VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later.', + ), + }); + }, }, + }, + computed: { noteDictionary() { return this.discussions .flatMap((x) => x.notes) @@ -94,56 +137,19 @@ export default { }; }, }, - created() { - this.fetchDiscussions(); - }, updated() { this.$nextTick(() => { initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, beforeDestroy() { - if (this.poll) this.poll.stop(); + if (this.poll) { + this.poll.stop(); + } }, methods: { - dateToSeconds(date) { - return Date.parse(date) / 1000; - }, - fetchDiscussions() { - // 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 - .get(this.vulnerability.discussionsUrl) - .then(({ data, headers: { date } }) => { - this.discussionsDictionary = data.reduce((acc, discussion) => { - acc[discussion.id] = convertObjectPropsToCamelCase(discussion, { deep: true }); - return acc; - }, {}); - - this.lastFetchedAt = this.dateToSeconds(date); - - if (!this.poll) this.createNotesPoll(); - - if (!Visibility.hidden()) { - // delays the initial request by 6 seconds - this.poll.makeDelayedRequest(6 * 1000); - } - - Visibility.change(() => { - if (Visibility.hidden()) { - this.poll.stop(); - } else { - this.poll.restart(); - } - }); - }) - .catch(() => { - createFlash({ - message: s__( - 'VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later.', - ), - }); - }); + findDiscussion(id) { + return this.discussions.find((d) => d.id === id); }, createNotesPoll() { // note: this polling call will be replaced when migrating the vulnerability details page to GraphQL @@ -159,48 +165,46 @@ export default { successCallback: ({ data: { notes, last_fetched_at: lastFetchedAt } }) => { this.updateNotes(convertObjectPropsToCamelCase(notes, { deep: true })); this.lastFetchedAt = lastFetchedAt; + this.notesLoading = false; }, - errorCallback: () => + errorCallback: () => { + this.notesLoading = false; createFlash({ message: __('Something went wrong while fetching latest comments.'), - }), + }); + }, }); }, updateNotes(notes) { - let isVulnerabilityStateChanged = false; + let shallEmitVulnerabilityChangedEvent; notes.forEach((note) => { + const discussion = this.findDiscussion(note.discussionId); // If the note exists, update it. if (this.noteDictionary[note.id]) { - const updatedDiscussion = { ...this.discussionsDictionary[note.discussionId] }; - updatedDiscussion.notes = updatedDiscussion.notes.map((curr) => - curr.id === note.id ? note : curr, - ); - this.discussionsDictionary[note.discussionId] = updatedDiscussion; + 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 (this.discussionsDictionary[note.discussionId]) { - const updatedDiscussion = { ...this.discussionsDictionary[note.discussionId] }; - updatedDiscussion.notes.push(note); - this.discussionsDictionary[note.discussionId] = updatedDiscussion; + else if (discussion) { + discussion.notes.push(note); } // If the discussion doesn't exist, create it. else { - const newDiscussion = { + this.discussions.push({ id: note.discussionId, replyId: note.discussionId, notes: [note], - }; - this.$set(this.discussionsDictionary, newDiscussion.id, newDiscussion); + }); // 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) { - isVulnerabilityStateChanged = true; + shallEmitVulnerabilityChangedEvent = true; } } }); - // Emit an event that tells the header to refresh the vulnerability. - if (isVulnerabilityStateChanged) { + + if (shallEmitVulnerabilityChangedEvent) { this.$emit('vulnerability-state-change'); } }, @@ -243,7 +247,8 @@ export default { </div> </div> <hr /> - <ul v-if="discussions.length" ref="historyList" class="notes discussion-body"> + <gl-loading-icon v-if="notesLoading" /> + <ul v-else-if="discussions.length" class="notes discussion-body"> <history-entry v-for="discussion in discussions" :key="discussion.id" diff --git a/ee/spec/frontend/vulnerabilities/footer_spec.js b/ee/spec/frontend/vulnerabilities/footer_spec.js index 6d6cbb7ac2a4109d096e364c0568ce978d74da46..2128b5ea22abfdb21b8543addddff79f2af8ac31 100644 --- a/ee/spec/frontend/vulnerabilities/footer_spec.js +++ b/ee/spec/frontend/vulnerabilities/footer_spec.js @@ -1,6 +1,9 @@ -import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import Api from 'ee/api'; +import vulnerabilityDiscussionsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_discussions.query.graphql'; import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue'; import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue'; import VulnerabilityFooter from 'ee/vulnerabilities/components/footer.vue'; @@ -10,20 +13,25 @@ import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue'; import RelatedJiraIssues from 'ee/vulnerabilities/components/related_jira_issues.vue'; import StatusDescription from 'ee/vulnerabilities/components/status_description.vue'; import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import initUserPopovers from '~/user_popovers'; const mockAxios = new MockAdapter(axios); jest.mock('~/flash'); jest.mock('~/user_popovers'); +Vue.use(VueApollo); + describe('Vulnerability Footer', () => { let wrapper; const vulnerability = { id: 1, - discussionsUrl: '/discussions', notesUrl: '/notes', project: { fullPath: '/root/security-reports', @@ -35,244 +43,242 @@ describe('Vulnerability Footer', () => { pipeline: {}, }; - const createWrapper = (properties = {}, mountOptions = {}) => { - wrapper = shallowMount(VulnerabilityFooter, { + let discussion1; + let discussion2; + let notes; + + const discussionsSuccessHandler = (nodes) => + jest.fn().mockResolvedValue({ + data: { + vulnerability: { + id: `gid://gitlab/Vulnerability/${vulnerability.id}`, + discussions: { + nodes, + }, + }, + }, + }); + + const discussionsErrorHandler = () => + jest.fn().mockRejectedValue({ + errors: [{ message: 'Something went wrong' }], + }); + + const createNotesRequest = (notesArray, statusCode = 200) => { + return mockAxios + .onGet(vulnerability.notesUrl) + .replyOnce(statusCode, { notes: notesArray }, { date: Date.now() }); + }; + + const createWrapper = ({ properties, discussionsHandler, mountOptions } = {}) => { + createNotesRequest(notes); + + wrapper = shallowMountExtended(VulnerabilityFooter, { propsData: { vulnerability: { ...vulnerability, ...properties } }, + apolloProvider: createMockApollo([[vulnerabilityDiscussionsQuery, discussionsHandler]]), ...mountOptions, }); }; + const createWrapperWithDiscussions = (props) => { + createWrapper({ + ...props, + discussionsHandler: discussionsSuccessHandler([discussion1, discussion2]), + }); + }; + + const findDiscussions = () => wrapper.findAllComponents(HistoryEntry); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findMergeRequestNote = () => wrapper.findComponent(MergeRequestNote); + const findRelatedIssues = () => wrapper.findComponent(RelatedIssues); + const findRelatedJiraIssues = () => wrapper.findComponent(RelatedJiraIssues); + + beforeEach(() => { + discussion1 = { + id: 'gid://gitlab/Discussion/7b4aa2d000ec81ba374a29b3ca3ee4c5f274f9ab', + replyId: 'gid://gitlab/Discussion/7b4aa2d000ec81ba374a29b3ca3ee4c5f274f9ab', + }; + + discussion2 = { + id: 'gid://gitlab/Discussion/0656f86109dc755c99c288c54d154b9705aaa796', + replyId: 'gid://gitlab/Discussion/0656f86109dc755c99c288c54d154b9705aaa796', + }; + + notes = [ + { id: 100, note: 'some note', discussion_id: discussion1.id }, + { id: 200, note: 'another note', discussion_id: discussion2.id }, + ]; + }); + afterEach(() => { wrapper.destroy(); - wrapper = null; mockAxios.reset(); }); - describe('fetching discussions', () => { - it('calls the discussion url on if fetchDiscussions is called by the root', async () => { - createWrapper(); - jest.spyOn(axios, 'get'); - wrapper.vm.fetchDiscussions(); + describe('discussions and notes', () => { + const createWrapperAndFetchNotes = async () => { + createWrapperWithDiscussions(); + await axios.waitForAll(); + expect(findDiscussions()).toHaveLength(2); + expect(findDiscussions().at(0).props('discussion').notes).toHaveLength(1); + }; + const makePollRequest = async () => { + wrapper.vm.poll.makeRequest(); await axios.waitForAll(); + }; - expect(axios.get).toHaveBeenCalledTimes(1); + it('displays a loading spinner while fetching discussions', async () => { + createWrapperWithDiscussions(); + expect(findDiscussions().exists()).toBe(false); + expect(findLoadingIcon().exists()).toBe(true); + await axios.waitForAll(); + expect(findLoadingIcon().exists()).toBe(false); }); - }); - describe('solution card', () => { - it('does show solution card when there is one', () => { - const properties = { remediations: [{ diff: [{}] }], solution: 'some solution' }; - createWrapper(properties); + it('fetches discussions and notes on mount', async () => { + await createWrapperAndFetchNotes(); - expect(wrapper.find(SolutionCard).exists()).toBe(true); - expect(wrapper.find(SolutionCard).props()).toEqual({ - solution: properties.solution, - remediation: properties.remediations[0], - hasDownload: true, - hasMr: vulnerability.hasMr, + expect(findDiscussions().at(0).props()).toEqual({ + discussion: { ...discussion1, notes: [convertObjectPropsToCamelCase(notes[0])] }, + notesUrl: vulnerability.notesUrl, }); - }); - - it('does not show solution card when there is not one', () => { - createWrapper(); - expect(wrapper.find(SolutionCard).exists()).toBe(false); - }); - }); - describe('merge request note', () => { - const mergeRequestNote = () => wrapper.find(MergeRequestNote); - - it('does not show merge request note when a merge request does not exist for the vulnerability', () => { - createWrapper(); - expect(mergeRequestNote().exists()).toBe(false); - }); - - it('shows merge request note when a merge request exists for the vulnerability', () => { - // The object itself does not matter, we just want to make sure it's passed to the issue note. - const mergeRequestFeedback = {}; - - createWrapper({ mergeRequestFeedback }); - expect(mergeRequestNote().exists()).toBe(true); - expect(mergeRequestNote().props('feedback')).toBe(mergeRequestFeedback); + expect(findDiscussions().at(1).props()).toEqual({ + discussion: { ...discussion2, notes: [convertObjectPropsToCamelCase(notes[1])] }, + notesUrl: vulnerability.notesUrl, + }); }); - }); - - describe('state history', () => { - const discussionUrl = vulnerability.discussionsUrl; - - const historyList = () => wrapper.find({ ref: 'historyList' }); - const historyEntries = () => wrapper.findAll(HistoryEntry); - it('does not render the history list if there are no history items', () => { - mockAxios.onGet(discussionUrl).replyOnce(200, []); - createWrapper(); - expect(historyList().exists()).toBe(false); + it('calls initUserPopovers when the component is updated', async () => { + createWrapperWithDiscussions(); + expect(initUserPopovers).not.toHaveBeenCalled(); + await axios.waitForAll(); + expect(initUserPopovers).toHaveBeenCalled(); }); - it('renders the history list if there are history items', () => { - // The shape of this object doesn't matter for this test, we just need to verify that it's passed to the history - // entry. - const historyItems = [ - { id: 1, note: 'some note' }, - { id: 2, note: 'another note' }, - ]; - mockAxios.onGet(discussionUrl).replyOnce(200, historyItems, { date: Date.now() }); - createWrapper(); - - return axios.waitForAll().then(() => { - expect(historyList().exists()).toBe(true); - expect(historyEntries()).toHaveLength(2); - const entry1 = historyEntries().at(0); - const entry2 = historyEntries().at(1); - expect(entry1.props('discussion')).toEqual(historyItems[0]); - expect(entry2.props('discussion')).toEqual(historyItems[1]); + it('shows an error the discussions could not be retrieved', async () => { + createWrapper({ discussionsHandler: discussionsErrorHandler() }); + await waitForPromises(); + expect(createFlash).toHaveBeenCalledWith({ + message: + 'Something went wrong while trying to retrieve the vulnerability history. Please try again later.', }); }); - it('calls initUserPopovers when a new history item is retrieved', () => { - const historyItems = [{ id: 1, note: 'some note' }]; - mockAxios.onGet(discussionUrl).replyOnce(200, historyItems, { date: Date.now() }); + it('adds a new note to an existing discussion if the note does not exist', async () => { + await createWrapperAndFetchNotes(); - expect(initUserPopovers).not.toHaveBeenCalled(); - createWrapper(); + // Fetch a new note + const note = { id: 101, note: 'new note', discussion_id: discussion1.id }; + createNotesRequest([note]); + await makePollRequest(); - return axios.waitForAll().then(() => { - expect(initUserPopovers).toHaveBeenCalled(); - }); + expect(findDiscussions()).toHaveLength(2); + expect(findDiscussions().at(0).props('discussion').notes[1].note).toBe(note.note); }); - it('shows an error the history list could not be retrieved', () => { - mockAxios.onGet(discussionUrl).replyOnce(500); - createWrapper(); + it('updates an existing note if it already exists', async () => { + await createWrapperAndFetchNotes(); - return axios.waitForAll().then(() => { - expect(createFlash).toHaveBeenCalledTimes(1); - }); - }); - - describe('new notes polling', () => { - jest.useFakeTimers(); + const note = { ...notes[0], note: 'updated note' }; + createNotesRequest([note]); + await makePollRequest(); - const getDiscussion = (entries, index) => entries.at(index).props('discussion'); - const createNotesRequest = (...notes) => - mockAxios - .onGet(vulnerability.notes_url) - .replyOnce(200, { notes, lastFetchedAt: Date.now() }); + expect(findDiscussions()).toHaveLength(2); + expect(findDiscussions().at(0).props('discussion').notes).toHaveLength(1); + expect(findDiscussions().at(0).props('discussion').notes[0].note).toBe(note.note); + }); - // Following #217184 the vulnerability polling uses an initial timeout - // which we need to run and then wait for the subsequent request. - const startTimeoutsAndAwaitRequests = async () => { - expect(setTimeout).toHaveBeenCalledTimes(1); - jest.runAllTimers(); + it('creates a new discussion with a new note if the discussion does not exist', async () => { + await createWrapperAndFetchNotes(); - return axios.waitForAll(); + const note = { + id: 300, + note: 'new note on a new discussion', + discussion_id: 'new-discussion-id', }; - beforeEach(() => { - const historyItems = [ - { id: 1, notes: [{ id: 100, note: 'some note', discussion_id: 1 }] }, - { id: 2, notes: [{ id: 200, note: 'another note', discussion_id: 2 }] }, - ]; - mockAxios.onGet(discussionUrl).replyOnce(200, historyItems, { date: Date.now() }); - createWrapper(); - }); - - it('updates an existing note if it already exists', () => { - const note = { id: 100, note: 'updated note', discussion_id: 1 }; - createNotesRequest(note); + createNotesRequest([note]); + await makePollRequest(); - return axios.waitForAll().then(async () => { - await startTimeoutsAndAwaitRequests(); - - const entries = historyEntries(); - expect(entries).toHaveLength(2); - const discussion = getDiscussion(entries, 0); - expect(discussion.notes.length).toBe(1); - expect(discussion.notes[0].note).toBe('updated note'); - }); - }); + expect(findDiscussions()).toHaveLength(3); + expect(findDiscussions().at(2).props('discussion').notes).toHaveLength(1); + expect(findDiscussions().at(2).props('discussion').notes[0].note).toBe(note.note); + }); - it('adds a new note to an existing discussion if the note does not exist', () => { - const note = { id: 101, note: 'new note', discussion_id: 1 }; - createNotesRequest(note); + it('shows an error if the notes poll fails', async () => { + await createWrapperAndFetchNotes(); - return axios.waitForAll().then(async () => { - await startTimeoutsAndAwaitRequests(); + createNotesRequest([], 500); + await makePollRequest(); - const entries = historyEntries(); - expect(entries).toHaveLength(2); - const discussion = getDiscussion(entries, 0); - expect(discussion.notes.length).toBe(2); - expect(discussion.notes[1].note).toBe('new note'); - }); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while fetching latest comments.', }); + }); - it('creates a new discussion with a new note if the discussion does not exist', () => { - const note = { id: 300, note: 'new note on a new discussion', discussion_id: 3 }; - createNotesRequest(note); + it('emits the vulnerability-state-change event when the system note is new', async () => { + await createWrapperAndFetchNotes(); - return axios.waitForAll().then(async () => { - await startTimeoutsAndAwaitRequests(); + const handler = jest.fn(); + wrapper.vm.$on('vulnerability-state-change', handler); - const entries = historyEntries(); - expect(entries).toHaveLength(3); - const discussion = getDiscussion(entries, 2); - expect(discussion.notes.length).toBe(1); - expect(discussion.notes[0].note).toBe('new note on a new discussion'); - }); - }); - - it('calls initUserPopovers when a new note is retrieved', () => { - expect(initUserPopovers).not.toHaveBeenCalled(); - const note = { id: 300, note: 'new note on a new discussion', discussion_id: 3 }; - createNotesRequest(note); + const note = { system: true, id: 1, discussion_id: 'some-new-discussion-id' }; + createNotesRequest([note]); + await makePollRequest(); - return axios.waitForAll().then(() => { - expect(initUserPopovers).toHaveBeenCalled(); - }); - }); - - it('shows an error if the notes poll fails', () => { - mockAxios.onGet(vulnerability.notes_url).replyOnce(500); + expect(handler).toHaveBeenCalledTimes(1); + }); + }); - return axios.waitForAll().then(async () => { - await startTimeoutsAndAwaitRequests(); + describe('solution card', () => { + it('does show solution card when there is one', () => { + const properties = { remediations: [{ diff: [{}] }], solution: 'some solution' }; + createWrapper({ properties, discussionsHandler: discussionsSuccessHandler([]) }); - expect(historyEntries()).toHaveLength(2); - expect(mockAxios.history.get).toHaveLength(2); - expect(createFlash).toHaveBeenCalled(); - }); + expect(wrapper.find(SolutionCard).exists()).toBe(true); + expect(wrapper.find(SolutionCard).props()).toEqual({ + solution: properties.solution, + remediation: properties.remediations[0], + hasDownload: true, + hasMr: vulnerability.hasMr, }); + }); - it('emits the vulnerability-state-change event when the system note is new', async () => { - const handler = jest.fn(); - wrapper.vm.$on('vulnerability-state-change', handler); - - const note = { system: true, id: 1, discussion_id: 3 }; - createNotesRequest(note); + it('does not show solution card when there is not one', () => { + createWrapper(); + expect(wrapper.find(SolutionCard).exists()).toBe(false); + }); + }); - await axios.waitForAll(); + describe('merge request note', () => { + it('does not show merge request note when a merge request does not exist for the vulnerability', () => { + createWrapper(); + expect(findMergeRequestNote().exists()).toBe(false); + }); - await startTimeoutsAndAwaitRequests(); + it('shows merge request note when a merge request exists for the vulnerability', () => { + // The object itself does not matter, we just want to make sure it's passed to the issue note. + const mergeRequestFeedback = {}; - expect(handler).toHaveBeenCalledTimes(1); - }); + createWrapper({ properties: { mergeRequestFeedback } }); + expect(findMergeRequestNote().exists()).toBe(true); + expect(findMergeRequestNote().props('feedback')).toBe(mergeRequestFeedback); }); }); describe('related issues', () => { - const relatedIssues = () => wrapper.find(RelatedIssues); - it('has the correct props', () => { const endpoint = Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace( ':id', vulnerability.id, ); + createWrapper(); - expect(relatedIssues().exists()).toBe(true); - expect(relatedIssues().props()).toMatchObject({ + expect(findRelatedIssues().exists()).toBe(true); + expect(findRelatedIssues().props()).toMatchObject({ endpoint, canModifyRelatedIssues: vulnerability.canModifyRelatedIssues, projectPath: vulnerability.project.fullPath, @@ -282,8 +288,6 @@ describe('Vulnerability Footer', () => { }); describe('related jira issues', () => { - const relatedJiraIssues = () => wrapper.find(RelatedJiraIssues); - describe.each` createJiraIssueUrl | shouldShowRelatedJiraIssues ${'http://foo'} | ${true} @@ -292,20 +296,19 @@ describe('Vulnerability Footer', () => { 'with "createJiraIssueUrl" set to "$createJiraIssueUrl"', ({ createJiraIssueUrl, shouldShowRelatedJiraIssues }) => { beforeEach(() => { - createWrapper( - {}, - { + createWrapper({ + mountOptions: { provide: { createJiraIssueUrl, }, }, - ); + }); }); it(`${ shouldShowRelatedJiraIssues ? 'should' : 'should not' } show related Jira issues`, () => { - expect(relatedJiraIssues().exists()).toBe(shouldShowRelatedJiraIssues); + expect(findRelatedJiraIssues().exists()).toBe(shouldShowRelatedJiraIssues); }); }, ); @@ -319,7 +322,7 @@ describe('Vulnerability Footer', () => { it.each(vulnerabilityStates)( `shows detection note when vulnerability state is '%s'`, (state) => { - createWrapper({ state }); + createWrapper({ properties: { state } }); expect(detectionNote().exists()).toBe(true); expect(statusDescription().props('vulnerability')).toEqual({ @@ -337,7 +340,7 @@ describe('Vulnerability Footer', () => { describe('when a vulnerability contains a details property', () => { beforeEach(() => { - createWrapper({ details: mockDetails }); + createWrapper({ properties: { details: mockDetails } }); }); it('renders the report section', () => {