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', () => {