From 3ea284e67eca545998d34304cb3d501aa3cefe0c Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 10 May 2023 17:35:47 -0600 Subject: [PATCH 1/8] Add a global utility for message I/O in the Code Review area The purpose of this is to separate generic things like "socket messages" and "observable streams" out from being connected to "a component." Sometimes, it might make sense to connect a socket directly to a component's data or to a data store. In those cases, the `apollo` key of a Vue makes the most sense. However, sockets and observables are useful outside of just setting a property or updating data. In this case, we're attaching to a *signal*. Signals can be used in powerful applications like modifying a state machine, or in simple applications like how this is used, where we just call a method. Because we've chosen Apollo as our tool for communicating across Websockets, there's quite a bit of overhead associated with this signaling system, but by abstracting signal IO to a central place not in any particular component, we can re-use the code anywhere in the application. --- app/assets/javascripts/code_review/signals.js | 44 +++++++++++++++++++ app/assets/javascripts/diffs/constants.js | 1 + .../queries/merge_request.query.graphql | 9 ++++ ...erge_request_prepared.subscription.graphql | 8 ++++ 4 files changed, 62 insertions(+) create mode 100644 app/assets/javascripts/code_review/signals.js create mode 100644 app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql create mode 100644 app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql diff --git a/app/assets/javascripts/code_review/signals.js b/app/assets/javascripts/code_review/signals.js new file mode 100644 index 00000000000000..ebcabbd45cd1f8 --- /dev/null +++ b/app/assets/javascripts/code_review/signals.js @@ -0,0 +1,44 @@ +import createApolloClient from '../lib/graphql'; + +import { getDerivedMergeRequestInformation } from '../diffs/utils/merge_request'; +import { EVT_MR_PREPARED } from '../diffs/constants'; + +import getMr from '../graphql_shared/queries/merge_request.query.graphql'; +import mrPreparation from '../graphql_shared/subscriptions/merge_request_prepared.subscription.graphql'; + +async function observeMergeRequestFinishingPreparation({ apollo, signaler }) { + const { namespace, project, id: iid } = getDerivedMergeRequestInformation({ + endpoint: document.location.pathname, + }); + const projectPath = `${namespace}/${project}`; + + if (projectPath && iid) { + const currentStatus = await apollo.query({ + query: getMr, + variables: { projectPath, iid }, + }); + const { id: gqlMrId, preparedAt } = currentStatus.data.project.mergeRequest; + let preparationObservable; + let preparationSubscriber; + + if (!preparedAt) { + preparationObservable = apollo.subscribe({ + query: mrPreparation, + variables: { + issuableId: gqlMrId, + }, + }); + + preparationSubscriber = preparationObservable.subscribe((preparationUpdate) => { + if (preparationUpdate.data.mergeRequestMergeStatusUpdated?.preparedAt) { + signaler.$emit(EVT_MR_PREPARED); + preparationSubscriber.unsubscribe(); + } + }); + } + } +} + +export async function start({ signalBus, apolloClient = createApolloClient() }) { + await observeMergeRequestFinishingPreparation({ signaler: signalBus, apollo: apolloClient }); +} diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index a459def6b4b9f0..063e36fa7fb30a 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -79,6 +79,7 @@ export const RENAMED_DIFF_TRANSITIONS = { }; // MR Diffs known events +export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished'; export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles'; export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart'; export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd'; diff --git a/app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql b/app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql new file mode 100644 index 00000000000000..a6ef593516297b --- /dev/null +++ b/app/assets/javascripts/graphql_shared/queries/merge_request.query.graphql @@ -0,0 +1,9 @@ +query mergeRequestId($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + id + mergeRequest(iid: $iid) { + id + preparedAt + } + } +} diff --git a/app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql b/app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql new file mode 100644 index 00000000000000..ba658f56ebd610 --- /dev/null +++ b/app/assets/javascripts/graphql_shared/subscriptions/merge_request_prepared.subscription.graphql @@ -0,0 +1,8 @@ +subscription mergeRequestPrepared($issuableId: IssuableID!) { + mergeRequestMergeStatusUpdated(issuableId: $issuableId) { + ... on MergeRequest { + id + preparedAt + } + } +} -- GitLab From 3614e516f5e9ef42c74557d5432a2a8866923ada Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Tue, 9 May 2023 16:45:33 -0600 Subject: [PATCH 2/8] Listen for the MR being prepared and reload the page --- app/assets/javascripts/diffs/components/app.vue | 3 +++ app/assets/javascripts/diffs/store/actions.js | 2 +- app/assets/javascripts/pages/projects/merge_requests/page.js | 3 +++ locale/gitlab.pot | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index bfabac3123b7d8..4c4ec85f9684c5 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -41,6 +41,7 @@ import { TRACKING_WHITESPACE_HIDE, TRACKING_SINGLE_FILE_MODE, TRACKING_MULTIPLE_FILES_MODE, + EVT_MR_PREPARED, } from '../constants'; import diffsEventHub from '../event_hub'; @@ -470,8 +471,10 @@ export default { diffsEventHub.$on('diffFilesModified', this.setDiscussions); notesEventHub.$on('fetchedNotesData', this.rereadNoteHash); } + diffsEventHub.$on(EVT_MR_PREPARED, this.reloadPage); }, unsubscribeFromEvents() { + diffsEventHub.$off(EVT_MR_PREPARED, this.reloadPage); if (this.glFeatures.singleFileFileByFile) { notesEventHub.$off('fetchedNotesData', this.rereadNoteHash); diffsEventHub.$off('diffFilesModified', this.setDiscussions); diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 2a7b4da684b43a..9f6ebbf9488077 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -288,7 +288,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { .catch((error) => { if (error.response.status === HTTP_STATUS_NOT_FOUND) { createAlert({ - message: __('Building your merge request. Wait a few moments, then refresh this page.'), + message: __('Building your merge request. This page will refresh when the build has completed.'), variant: VARIANT_WARNING, }); } else { diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js index fbd45f4bd7d3f9..552e75da9b8f85 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/page.js +++ b/app/assets/javascripts/pages/projects/merge_requests/page.js @@ -3,6 +3,8 @@ import VueApollo from 'vue-apollo'; import initMrNotes from 'ee_else_ce/mr_notes'; import StickyHeader from '~/merge_requests/components/sticky_header.vue'; import { initIssuableHeaderWarnings } from '~/issuable'; +import { start as startCodeReviewMessaging } from '~/code_review/signals'; +import diffsEventHub from '~/diffs/event_hub'; import store from '~/mr_notes/stores'; import initSidebarBundle from '~/sidebar/sidebar_bundle'; import { apolloProvider } from '~/graphql_shared/issuable_client'; @@ -15,6 +17,7 @@ Vue.use(VueApollo); export function initMrPage() { initMrNotes(); initShow(); + startCodeReviewMessaging({ signalBus: diffsEventHub }); } requestIdleCallback(() => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8f934560f07fc5..71d9cd344e9425 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8167,7 +8167,7 @@ msgstr "" msgid "BuildArtifacts|Loading artifacts" msgstr "" -msgid "Building your merge request. Wait a few moments, then refresh this page." +msgid "Building your merge request. This page will refresh when the build has completed." msgstr "" msgid "Built-in" -- GitLab From 63e0fdd98c76069b64c22825aa2c205e2609599b Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 10 May 2023 11:58:58 -0600 Subject: [PATCH 3/8] Add TW recommendations --- app/assets/javascripts/diffs/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 9f6ebbf9488077..ae6bd224e50731 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -288,7 +288,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { .catch((error) => { if (error.response.status === HTTP_STATUS_NOT_FOUND) { createAlert({ - message: __('Building your merge request. This page will refresh when the build has completed.'), + message: __('Building your merge request… This page will refresh when the build is complete.'), variant: VARIANT_WARNING, }); } else { -- GitLab From 0af54fb94d3b7786dae7030178ecab111effec3f Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 10 May 2023 18:02:38 -0600 Subject: [PATCH 4/8] Switch to live reload This also means I need to maintain a closure over the alert message and also close it on the same signal. --- app/assets/javascripts/diffs/components/app.vue | 4 ++-- app/assets/javascripts/diffs/store/actions.js | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 4c4ec85f9684c5..02307150e2f52c 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -471,10 +471,10 @@ export default { diffsEventHub.$on('diffFilesModified', this.setDiscussions); notesEventHub.$on('fetchedNotesData', this.rereadNoteHash); } - diffsEventHub.$on(EVT_MR_PREPARED, this.reloadPage); + diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData); }, unsubscribeFromEvents() { - diffsEventHub.$off(EVT_MR_PREPARED, this.reloadPage); + diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData); if (this.glFeatures.singleFileFileByFile) { notesEventHub.$off('fetchedNotesData', this.rereadNoteHash); diffsEventHub.$off('diffFilesModified', this.setDiscussions); diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index ae6bd224e50731..91c449da0fffdc 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -49,6 +49,7 @@ import { TRACKING_CLICK_SINGLE_FILE_SETTING, TRACKING_SINGLE_FILE_MODE, TRACKING_MULTIPLE_FILES_MODE, + EVT_MR_PREPARED, } from '../constants'; import { DISCUSSION_SINGLE_DIFF_FAILED, LOAD_SINGLE_DIFF_FAILED } from '../i18n'; import eventHub from '../event_hub'; @@ -287,10 +288,14 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { }) .catch((error) => { if (error.response.status === HTTP_STATUS_NOT_FOUND) { - createAlert({ - message: __('Building your merge request… This page will refresh when the build is complete.'), + const alert = createAlert({ + message: __( + 'Building your merge request… This page will refresh when the build is complete.', + ), variant: VARIANT_WARNING, }); + + eventHub.$once(EVT_MR_PREPARED, () => alert.dismiss()); } else { throw error; } -- GitLab From 512c383bc3389c05cfb4b3410d7f57a87f64330e Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 10 May 2023 18:26:04 -0600 Subject: [PATCH 5/8] Update UI text to match new behavior --- app/assets/javascripts/diffs/store/actions.js | 2 +- locale/gitlab.pot | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 91c449da0fffdc..0668551902a7b9 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -290,7 +290,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { if (error.response.status === HTTP_STATUS_NOT_FOUND) { const alert = createAlert({ message: __( - 'Building your merge request… This page will refresh when the build is complete.', + 'Building your merge request… This page will update when the build is complete.', ), variant: VARIANT_WARNING, }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 71d9cd344e9425..118b8a4d9fc307 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8167,7 +8167,7 @@ msgstr "" msgid "BuildArtifacts|Loading artifacts" msgstr "" -msgid "Building your merge request. This page will refresh when the build has completed." +msgid "Building your merge request… This page will update when the build is complete." msgstr "" msgid "Built-in" -- GitLab From ce6282522f227afa8a8a9a2dc4ddf75dec917ea0 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Thu, 11 May 2023 13:21:58 -0600 Subject: [PATCH 6/8] Fix existing tests for new behavior --- spec/frontend/diffs/store/actions_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 7f9cd1a274d398..06061a965f7752 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -410,7 +410,7 @@ describe('DiffsStoreActions', () => { expect(createAlert).toHaveBeenCalledTimes(1); expect(createAlert).toHaveBeenCalledWith({ message: expect.stringMatching( - 'Building your merge request. Wait a few moments, then refresh this page.', + 'Building your merge request… This page will update when the build is complete.', ), variant: 'warning', }); -- GitLab From 32cfc418ac2ca046022d89d7e186df9a5aded7fc Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Thu, 11 May 2023 17:39:38 -0600 Subject: [PATCH 7/8] Test the preparation event causing the alert to close --- spec/frontend/diffs/store/actions_spec.js | 54 ++++++++++++++++------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 06061a965f7752..f883aea764fd20 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -9,6 +9,7 @@ import { DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, + EVT_MR_PREPARED, } from '~/diffs/constants'; import { LOAD_SINGLE_DIFF_FAILED } from '~/diffs/i18n'; import * as diffActions from '~/diffs/store/actions'; @@ -396,23 +397,46 @@ describe('DiffsStoreActions', () => { ); }); - it('should show a warning on 404 reponse', async () => { - mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND); + describe('on a 404 response', () => { + let dismissAlert; - await testAction( - diffActions.fetchDiffFilesMeta, - {}, - { endpointMetadata, diffViewType: 'inline', showWhitespace: true }, - [{ type: types.SET_LOADING, payload: true }], - [], - ); + beforeAll(() => { + dismissAlert = jest.fn(); - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ - message: expect.stringMatching( - 'Building your merge request… This page will update when the build is complete.', - ), - variant: 'warning', + mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND); + createAlert.mockImplementation(() => ({ dismiss: dismissAlert })); + }); + + it('should show a warning', async () => { + await testAction( + diffActions.fetchDiffFilesMeta, + {}, + { endpointMetadata, diffViewType: 'inline', showWhitespace: true }, + [{ type: types.SET_LOADING, payload: true }], + [], + ); + + expect(createAlert).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledWith({ + message: expect.stringMatching( + 'Building your merge request… This page will update when the build is complete.', + ), + variant: 'warning', + }); + }); + + it("should attempt to close the alert if the MR reports that it's been prepared", async () => { + await testAction( + diffActions.fetchDiffFilesMeta, + {}, + { endpointMetadata, diffViewType: 'inline', showWhitespace: true }, + [{ type: types.SET_LOADING, payload: true }], + [], + ); + + diffsEventHub.$emit(EVT_MR_PREPARED); + + expect(dismissAlert).toHaveBeenCalled(); }); }); -- GitLab From 40663b2bb19368559d4d56907ef3f0141640c4e0 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Thu, 11 May 2023 23:16:17 -0600 Subject: [PATCH 8/8] Test the new global Code Review signaling IO behavior --- app/assets/javascripts/code_review/signals.js | 9 +- spec/frontend/code_review/signals_spec.js | 145 ++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 spec/frontend/code_review/signals_spec.js diff --git a/app/assets/javascripts/code_review/signals.js b/app/assets/javascripts/code_review/signals.js index ebcabbd45cd1f8..101b7996bb5a45 100644 --- a/app/assets/javascripts/code_review/signals.js +++ b/app/assets/javascripts/code_review/signals.js @@ -6,6 +6,10 @@ import { EVT_MR_PREPARED } from '../diffs/constants'; import getMr from '../graphql_shared/queries/merge_request.query.graphql'; import mrPreparation from '../graphql_shared/subscriptions/merge_request_prepared.subscription.graphql'; +function required(name) { + throw new Error(`${name} is a required argument`); +} + async function observeMergeRequestFinishingPreparation({ apollo, signaler }) { const { namespace, project, id: iid } = getDerivedMergeRequestInformation({ endpoint: document.location.pathname, @@ -39,6 +43,9 @@ async function observeMergeRequestFinishingPreparation({ apollo, signaler }) { } } -export async function start({ signalBus, apolloClient = createApolloClient() }) { +export async function start({ + signalBus = required('signalBus'), + apolloClient = createApolloClient(), +} = {}) { await observeMergeRequestFinishingPreparation({ signaler: signalBus, apollo: apolloClient }); } diff --git a/spec/frontend/code_review/signals_spec.js b/spec/frontend/code_review/signals_spec.js new file mode 100644 index 00000000000000..03c3580860eed5 --- /dev/null +++ b/spec/frontend/code_review/signals_spec.js @@ -0,0 +1,145 @@ +import { start } from '~/code_review/signals'; + +import diffsEventHub from '~/diffs/event_hub'; +import { EVT_MR_PREPARED } from '~/diffs/constants'; +import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request'; + +jest.mock('~/diffs/utils/merge_request'); + +describe('~/code_review', () => { + const io = diffsEventHub; + + beforeAll(() => { + getDerivedMergeRequestInformation.mockImplementation(() => ({ + namespace: 'x', + project: 'y', + id: '1', + })); + }); + + describe('start', () => { + it.each` + description | argument + ${'no event hub is provided'} | ${{}} + ${'no parameters are provided'} | ${undefined} + `('throws an error if $description', async ({ argument }) => { + await expect(() => start(argument)).rejects.toThrow('signalBus is a required argument'); + }); + + describe('observeMergeRequestFinishingPreparation', () => { + const callArgs = {}; + const apollo = {}; + let querySpy; + let apolloSubscribeSpy; + let subscribeSpy; + let nextSpy; + let unsubscribeSpy; + let observable; + + beforeEach(() => { + querySpy = jest.fn(); + apolloSubscribeSpy = jest.fn(); + subscribeSpy = jest.fn(); + unsubscribeSpy = jest.fn(); + nextSpy = jest.fn(); + observable = { + next: nextSpy, + subscribe: subscribeSpy.mockReturnValue({ + unsubscribe: unsubscribeSpy, + }), + }; + + querySpy.mockResolvedValue({ + data: { project: { mergeRequest: { id: 'gql:id:1', preparedAt: 'x' } } }, + }); + apolloSubscribeSpy.mockReturnValue(observable); + + apollo.query = querySpy; + apollo.subscribe = apolloSubscribeSpy; + + callArgs.signalBus = io; + callArgs.apolloClient = apollo; + }); + + it('does not query at all if the page does not seem like a merge request', async () => { + getDerivedMergeRequestInformation.mockImplementationOnce(() => ({})); + + await start(callArgs); + + expect(querySpy).not.toHaveBeenCalled(); + expect(apolloSubscribeSpy).not.toHaveBeenCalled(); + }); + + describe('on a merge request page', () => { + it('requests the preparedAt (and id) for the current merge request', async () => { + await start(callArgs); + + expect(querySpy).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + projectPath: 'x/y', + iid: '1', + }, + }), + ); + }); + + it('does not subscribe to any updates if the preparedAt value is already populated', async () => { + await start(callArgs); + + expect(apolloSubscribeSpy).not.toHaveBeenCalled(); + }); + + describe('if the merge request is still asynchronously preparing', () => { + beforeEach(() => { + querySpy.mockResolvedValue({ + data: { project: { mergeRequest: { id: 'gql:id:1', preparedAt: null } } }, + }); + }); + + it('subscribes to updates', async () => { + await start(callArgs); + + expect(apolloSubscribeSpy).toHaveBeenCalledWith( + expect.objectContaining({ variables: { issuableId: 'gql:id:1' } }), + ); + expect(observable.subscribe).toHaveBeenCalled(); + }); + + describe('when the MR has been updated', () => { + let emitSpy; + let behavior; + + beforeEach(() => { + emitSpy = jest.spyOn(diffsEventHub, '$emit'); + nextSpy.mockImplementation((data) => behavior?.(data)); + subscribeSpy.mockImplementation((handler) => { + behavior = handler; + + return { unsubscribe: unsubscribeSpy }; + }); + }); + + it('does nothing if the MR has not yet finished preparing', async () => { + await start(callArgs); + + observable.next({ data: { mergeRequestMergeStatusUpdated: { preparedAt: null } } }); + + expect(unsubscribeSpy).not.toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('emits an event and unsubscribes when the MR is prepared', async () => { + await start(callArgs); + + observable.next({ data: { mergeRequestMergeStatusUpdated: { preparedAt: 'x' } } }); + + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(EVT_MR_PREPARED); + }); + }); + }); + }); + }); + }); +}); -- GitLab