diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js index a8d2736531a515a2d3ea64259cb9ce44a6b08f9e..24bc7017e060b9323d09ad14231f1240d81923c0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js @@ -1,6 +1,7 @@ export const COMPONENTS = { conflict: () => import('./conflicts.vue'), discussions_not_resolved: () => import('./unresolved_discussions.vue'), + draft_status: () => import('./draft.vue'), need_rebase: () => import('./rebase.vue'), default: () => import('./message.vue'), }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..537c975652f7ccea4552644bb70e0bc1bff11fc5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js @@ -0,0 +1,74 @@ +import createMockApollo from 'helpers/mock_apollo_helper'; +import draftStateQuery from '../../queries/states/draft.query.graphql'; +import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql'; +import Draft from './draft.vue'; + +const defaultRender = ({ apolloProvider, check, mr }) => ({ + components: { Draft }, + apolloProvider, + data() { + return { mr, check }; + }, + template: '', +}); + +const Template = ({ userPermissionUpdateMergeRequest }) => { + const requestHandlers = [ + [ + draftStateQuery, + () => + Promise.resolve({ + data: { + project: { + id: '1', + mergeRequest: { + draft: false, + id: '2', + title: 'MR title', + mergeableDiscussionsState: true, + userPermissions: { + updateMergeRequest: userPermissionUpdateMergeRequest, + }, + }, + }, + }, + }), + ], + [ + removeDraftMutation, + () => + Promise.resolve({ + data: { + mergeRequestSetDraft: { + mergeRequest: { + draft: false, + id: '2', + title: 'MR title', + mergeableDiscussionsState: true, + }, + errors: [], + }, + }, + }), + ], + ]; + const apolloProvider = createMockApollo(requestHandlers); + + return defaultRender({ + apolloProvider, + check: { + identifier: 'draft_status', + status: 'FAILED', + }, + }); +}; + +export const Default = Template.bind({}); +Default.args = { + userPermissionUpdateMergeRequest: true, +}; + +export default { + title: 'vue_merge_request_widget/merge_checks/draft', + component: Draft, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue new file mode 100644 index 0000000000000000000000000000000000000000..dbe0d2ac243b52586ec219048166fc0373ea25f5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue @@ -0,0 +1,169 @@ + + + diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js new file mode 100644 index 0000000000000000000000000000000000000000..de504af5fccdf37bd772e2ea78af3d4444a88bcf --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js @@ -0,0 +1,4 @@ +import { __, s__ } from '~/locale'; + +export const DRAFT_CHECK_ERROR = __('Something went wrong. Please try again.'); +export const DRAFT_CHECK_READY = s__('mrWidgetDraftCheck|Mark as ready'); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue index e91c76e7ff07a7f70c0d3365e9062bbf2c616bab..7f21445559a0ee30baf22917d683519505474617 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue @@ -8,7 +8,7 @@ const ICON_NAMES = { success: 'success', }; -const FAILURE_REASONS = { +export const FAILURE_REASONS = { broken_status: __('Cannot merge the source into the target branch, due to a conflict.'), ci_must_pass: __('Pipeline must succeed.'), conflict: __('Merge conflicts must be resolved.'), diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js index 77dc5b1d0daaa5a64ab81a07c40d63c1b1ee48a4..a1171fe5d256e368c4a790749a12164a82c23b61 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js @@ -41,6 +41,10 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => { identifier: 'CONFLICT', status: failed ? 'FAILED' : 'SUCCESS', }, + { + identifier: 'DRAFT_STATUS', + status: failed ? 'FAILED' : 'SUCCESS', + }, ], }, }, diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql index 54f2233439f55f34ef20d7c95b6ef3012d87f6ab..c1190a07ef84a3b23bd788c1379545eccb6fcd4f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql @@ -2,7 +2,10 @@ query mrUserPermission($projectPath: ID!, $iid: String!) { project(fullPath: $projectPath) { id mergeRequest(iid: $iid) { + draft id + mergeableDiscussionsState + title userPermissions { updateMergeRequest } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6872275f38490b22caa6f390e142aeda9baa683c..a364af32443d07a8ca169a6d67e3c6ace0a2dd63 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -57666,6 +57666,9 @@ msgstr "" msgid "mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}." msgstr "" +msgid "mrWidgetDraftCheck|Mark as ready" +msgstr "" + msgid "mrWidgetNothingToMerge|Merge request contains no changes" msgstr "" diff --git a/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..cc605c8c83deecbf265baa0720bdc3c6f01b77b7 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js @@ -0,0 +1,196 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; + +import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json'; + +import { createAlert } from '~/alert'; + +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +import MergeRequest from '~/merge_request'; + +import DraftCheck from '~/vue_merge_request_widget/components/checks/draft.vue'; +import { + DRAFT_CHECK_READY, + DRAFT_CHECK_ERROR, +} from '~/vue_merge_request_widget/components/checks/i18n'; +import { FAILURE_REASONS } from '~/vue_merge_request_widget/components/checks/message.vue'; + +import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql'; +import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql'; +import removeDraftMutation from '~/vue_merge_request_widget/queries/toggle_draft.mutation.graphql'; + +Vue.use(VueApollo); + +const TEST_PROJECT_ID = getStateQueryResponse.data.project.id; +const TEST_MR_ID = getStateQueryResponse.data.project.mergeRequest.id; +const TEST_MR_IID = '23'; +const TEST_MR_TITLE = 'Test MR Title'; +const TEST_PROJECT_PATH = 'lorem/ipsum'; + +jest.mock('~/alert'); +jest.mock('~/merge_request', () => ({ toggleDraftStatus: jest.fn() })); + +describe('~/vue_merge_request_widget/components/checks/draft.vue', () => { + let wrapper; + let apolloProvider; + + let draftQuerySpy; + let removeDraftMutationSpy; + + const findMarkReadyButton = () => wrapper.findByTestId('mark-as-ready-button'); + + const createDraftQueryResponse = (canUpdateMergeRequest) => ({ + data: { + project: { + __typename: 'Project', + id: TEST_PROJECT_ID, + mergeRequest: { + __typename: 'MergeRequest', + id: TEST_MR_ID, + draft: true, + title: TEST_MR_TITLE, + mergeableDiscussionsState: false, + userPermissions: { + updateMergeRequest: canUpdateMergeRequest, + }, + }, + }, + }, + }); + const createRemoveDraftMutationResponse = () => ({ + data: { + mergeRequestSetDraft: { + __typename: 'MergeRequestSetWipPayload', + errors: [], + mergeRequest: { + __typename: 'MergeRequest', + id: TEST_MR_ID, + title: TEST_MR_TITLE, + draft: false, + mergeableDiscussionsState: true, + }, + }, + }, + }); + + const createComponent = async () => { + wrapper = mountExtended(DraftCheck, { + apolloProvider, + propsData: { + mr: { + issuableId: TEST_MR_ID, + title: TEST_MR_TITLE, + iid: TEST_MR_IID, + targetProjectFullPath: TEST_PROJECT_PATH, + }, + check: { + identifier: 'draft_status', + status: 'FAILED', + }, + }, + }); + + await waitForPromises(); + + // why: draft.vue has some coupling that this query has been read before + // for some reason this has to happen **after** the component has mounted + // or apollo throws errors. + apolloProvider.defaultClient.cache.writeQuery({ + query: getStateQuery, + variables: { + projectPath: TEST_PROJECT_PATH, + iid: TEST_MR_IID, + }, + data: getStateQueryResponse.data, + }); + }; + + beforeEach(() => { + draftQuerySpy = jest.fn().mockResolvedValue(createDraftQueryResponse(true)); + removeDraftMutationSpy = jest.fn().mockResolvedValue(createRemoveDraftMutationResponse()); + + apolloProvider = createMockApollo([ + [draftQuery, draftQuerySpy], + [removeDraftMutation, removeDraftMutationSpy], + ]); + }); + + describe('when user can update MR', () => { + beforeEach(async () => { + await createComponent(); + }); + + it('renders text', () => { + const message = wrapper.text(); + expect(message).toContain(FAILURE_REASONS.draft_status); + }); + + it('renders mark ready button', () => { + expect(findMarkReadyButton().text()).toBe(DRAFT_CHECK_READY); + }); + + it('does not call remove draft mutation', () => { + expect(removeDraftMutationSpy).not.toHaveBeenCalled(); + }); + + describe('when mark ready button is clicked', () => { + beforeEach(async () => { + findMarkReadyButton().vm.$emit('click'); + + await waitForPromises(); + }); + + it('calls mutation spy', () => { + expect(removeDraftMutationSpy).toHaveBeenCalledWith({ + draft: false, + iid: TEST_MR_IID, + projectPath: TEST_PROJECT_PATH, + }); + }); + + it('does not create alert', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); + + it('calls toggleDraftStatus', () => { + expect(MergeRequest.toggleDraftStatus).toHaveBeenCalledWith(TEST_MR_TITLE, true); + }); + }); + + describe('when mutation fails and ready button is clicked', () => { + beforeEach(async () => { + removeDraftMutationSpy.mockRejectedValue(new Error('TEST FAIL')); + findMarkReadyButton().vm.$emit('click'); + + await waitForPromises(); + }); + + it('creates alert', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: DRAFT_CHECK_ERROR, + }); + }); + + it('does not call toggleDraftStatus', () => { + expect(MergeRequest.toggleDraftStatus).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when user cannot update MR', () => { + beforeEach(async () => { + draftQuerySpy.mockResolvedValue(createDraftQueryResponse(false)); + + createComponent(); + + await waitForPromises(); + }); + + it('does not render mark ready button', () => { + expect(findMarkReadyButton().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js index f46829539a8f7754b68d1218f9cb9c2c5b2ab667..509b5adb3b23053a7abc2347695f355b50db7e7b 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js @@ -42,6 +42,9 @@ describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () mergeRequest: { __typename: 'MergeRequest', id: TEST_MR_ID, + draft: true, + title: TEST_MR_TITLE, + mergeableDiscussionsState: false, userPermissions: { updateMergeRequest: canUpdateMergeRequest, },