Commit 44926ce2 authored by Phil Hughes's avatar Phil Hughes Committed by Igor Drozdov

Moves the auto merge state to GraphQL

Moves the auto merge failed and enabled states into
GraphQL for its state data.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/235715
parent d65e7145
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
import { __ } from '~/locale';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
name: 'MRWidgetAutoMergeEnabled',
apollo: {
state: {
query: autoMergeEnabledQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest,
},
},
components: {
MrWidgetAuthor,
statusIcon,
GlLoadingIcon,
GlSkeletonLoader,
},
mixins: [autoMergeMixin],
mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
......@@ -30,20 +46,47 @@ export default {
},
data() {
return {
state: {},
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
loading() {
return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
},
mergeUser() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.mergeUser;
}
return this.mr.setToAutoMergeBy;
},
targetBranch() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch;
},
shouldRemoveSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
}
return this.mr.shouldRemoveSourceBranch;
},
autoMergeStrategy() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy;
},
canRemoveSourceBranch() {
const {
shouldRemoveSourceBranch,
canRemoveSourceBranch,
mergeUserId,
currentUserId,
} = this.mr;
const { currentUserId } = this.mr;
const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql
? this.state.mergeUser?.id
: this.mr.mergeUserId;
const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql
? this.state.userPermissions.removeSourceBranch
: this.mr.canRemoveSourceBranch;
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
return (
!this.shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId
);
},
},
methods: {
......@@ -63,7 +106,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
auto_merge_strategy: this.mr.autoMergeStrategy,
auto_merge_strategy: this.autoMergeStrategy,
should_remove_source_branch: true,
};
......@@ -86,49 +129,64 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<h4 class="d-flex align-items-start">
<span class="gl-mr-3">
<span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span>
<mr-widget-author :author="mr.setToAutoMergeBy" />
<span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
</span>
<a
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="d-flex align-items-start">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<div v-if="loading" class="gl-w-full mr-conflict-loader">
<gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="3" width="24" height="24" rx="4" />
<rect x="32" y="7" width="150" height="16" rx="4" />
<rect x="190" y="7" width="144" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else>
<status-icon status="success" />
<div class="media-body">
<h4 class="gl-display-flex">
<span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{
statusTextBeforeAuthor
}}</span>
<mr-widget-author :author="mergeUser" />
<span class="js-status-text-after-author" data-testid="afterStatusText">{{
statusTextAfterAuthor
}}</span>
</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
@click.prevent="removeSourceBranch"
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</p>
</section>
</div>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a>
</p>
<p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
data-testid="removeSourceBranchButton"
@click.prevent="removeSourceBranch"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
</a>
</p>
</section>
</div>
</template>
</div>
</template>
<script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
name: 'MRWidgetAutoMergeFailed',
......@@ -10,6 +13,19 @@ export default {
GlLoadingIcon,
GlButton,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
mergeError: {
query: autoMergeFailedQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest?.mergeError,
},
},
props: {
mr: {
type: Object,
......@@ -18,6 +34,7 @@ export default {
},
data() {
return {
mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError,
isRefreshing: false,
};
},
......@@ -36,7 +53,7 @@ export default {
<status-icon status="warning" />
<div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center">
<span class="bold">
<template v-if="mr.mergeError">{{ mr.mergeError }}</template>
<template v-if="mergeError">{{ mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span>
<gl-button
......
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
name
username
webUrl
avatarUrl
}
targetBranch
shouldRemoveSourceBranch
forceRemoveSourceBranch
userPermissions {
removeSourceBranch
}
}
#import "./auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
mergeTrainsCount
}
}
}
query autoMergeFailedQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
mergeError
}
}
}
......@@ -175,6 +175,10 @@ module Types
calls_gitaly: true, description: 'Merge request commits excluding merge commits'
field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.'
field :auto_merge_strategy, GraphQL::STRING_TYPE, null: true,
description: 'Selected auto merge strategy'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request'
def approved_by
object.approved_by_users
......
......@@ -13786,6 +13786,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
autoMergeEnabled: Boolean!
"""
Selected auto merge strategy
"""
autoMergeStrategy: String
"""
Array of available auto merge strategies
"""
......@@ -14025,6 +14030,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
mergeTrainsCount: Int
"""
User who merged this merge request
"""
mergeUser: User
"""
Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)
"""
......
......@@ -37858,6 +37858,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "autoMergeStrategy",
"description": "Selected auto merge strategy",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "availableAutoMergeStrategies",
"description": "Array of available auto merge strategies",
......@@ -38509,6 +38523,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeUser",
"description": "User who merged this merge request",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeWhenPipelineSucceeds",
"description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)",
......@@ -2086,6 +2086,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `assignees` | UserConnection | Assignees of the merge request |
| `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
| `autoMergeStrategy` | String | Selected auto merge strategy |
| `availableAutoMergeStrategies` | String! => Array | Array of available auto merge strategies |
| `commitCount` | Int | Number of commits in the merge request |
| `commitsWithoutMergeCommits` | CommitConnection | Merge request commits excluding merge commits |
......@@ -2116,6 +2117,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
| `mergeStatus` | String | Status of the merge request |
| `mergeTrainsCount` | Int | |
| `mergeUser` | User | User who merged this merge request |
| `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) |
| `mergeable` | Boolean! | Indicates if the merge request is mergeable |
| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
......
......@@ -8,28 +8,27 @@ import { s__ } from '~/locale';
export default {
computed: {
statusTextBeforeAuthor() {
if (this.mr.autoMergeStrategy === MT_MERGE_STRATEGY) {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Added to the merge train by');
}
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
if (this.mr.autoMergeStrategy === MTWPS_MERGE_STRATEGY && this.mr.mergeTrainsCount === 0) {
const { mergeTrainsCount } = this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount === 0) {
return s__('mrWidget|to start a merge train when the pipeline succeeds');
} else if (
this.mr.autoMergeStrategy === MTWPS_MERGE_STRATEGY &&
this.mr.mergeTrainsCount !== 0
) {
} else if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount !== 0) {
return s__('mrWidget|to be added to the merge train when the pipeline succeeds');
} else if (this.mr.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
} else if (this.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
}
return '';
},
cancelButtonText() {
if (this.mr.autoMergeStrategy === MT_MERGE_STRATEGY) {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Remove from merge train');
}
......
#import "~/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
}
}
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'jest/helpers/vue_test_utils_helper';
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import eventHub from '~/vue_merge_request_widget/event_hub';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
let wrapper;
let mergeRequestWidgetGraphqlEnabled = false;
function convertPropsToGraphqlState(props) {
return {
autoMergeStrategy: props.autoMergeStrategy,
cancelAutoMergePath: 'http://text.com',
mergeUser: {
id: props.mergeUserId,
...props.setToAutoMergeBy,
},
targetBranch: props.targetBranch,
targetBranchCommitsPath: props.targetBranchPath,
shouldRemoveSourceBranch: props.shouldRemoveSourceBranch,
forceRemoveSourceBranch: props.shouldRemoveSourceBranch,
userPermissions: {
removeSourceBranch: props.canRemoveSourceBranch,
},
};
}
function factory(propsData) {
let state = {};
if (mergeRequestWidgetGraphqlEnabled) {
state = convertPropsToGraphqlState(propsData);
}
wrapper = extendedWrapper(
shallowMount(autoMergeEnabledComponent, {
propsData: {
mr: propsData,
service: new MRWidgetService({}),
},
data() {
return { state };
},
provide: { glFeatures: { mergeRequestWidgetGraphql: mergeRequestWidgetGraphqlEnabled } },
mocks: {
$apollo: {
queries: {
state: { loading: false },
},
},
},
}),
);
}
const targetBranchPath = '/foo/bar';
const targetBranch = 'foo';
const sha = '1EA2EZ34';
const defaultMrProps = () => ({
shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true,
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
describe('MRWidgetAutoMergeEnabled', () => {
let vm;
let oldWindowGl;
const targetBranchPath = '/foo/bar';
const targetBranch = 'foo';
const sha = '1EA2EZ34';
beforeEach(() => {
const Component = Vue.extend(autoMergeEnabledComponent);
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
oldWindowGl = window.gl;
......@@ -23,216 +84,234 @@ describe('MRWidgetAutoMergeEnabled', () => {
defaultAvatarUrl: 'no_avatar.png',
},
};
vm = mountComponent(Component, {
mr: {
shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true,
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
autoMergeStrategy: MWPS_MERGE_STRATEGY,
},
service: new MRWidgetService({}),
});
});
afterEach(() => {
vm.$destroy();
window.gl = oldWindowGl;
wrapper.destroy();
wrapper = null;
});
describe('computed', () => {
describe('canRemoveSourceBranch', () => {
it('should return true when user is able to remove source branch', () => {
expect(vm.canRemoveSourceBranch).toBeTruthy();
[true, false].forEach((mergeRequestWidgetGraphql) => {
describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
beforeEach(() => {
mergeRequestWidgetGraphqlEnabled = mergeRequestWidgetGraphql;
});
it('should return false when user id is not the same with who set the MWPS', () => {
vm.mr.mergeUserId = 2;
expect(vm.canRemoveSourceBranch).toBeFalsy();
vm.mr.currentUserId = 2;
expect(vm.canRemoveSourceBranch).toBeTruthy();
describe('computed', () => {
describe('canRemoveSourceBranch', () => {
it('should return true when user is able to remove source branch', () => {
factory({
...defaultMrProps(),
});
vm.mr.currentUserId = 3;
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
});
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
it.each`
mergeUserId | currentUserId
${2} | ${1}
${1} | ${2}
`(
'should return false when user id is not the same with who set the MWPS',
({ mergeUserId, currentUserId }) => {
factory({
...defaultMrProps(),
mergeUserId,
currentUserId,
});
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
},
);
it('should return false when shouldRemoveSourceBranch set to false', () => {
vm.mr.shouldRemoveSourceBranch = true;
it('should return false when shouldRemoveSourceBranch set to false', () => {
factory({
...defaultMrProps(),
shouldRemoveSourceBranch: true,
});
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
});
it('should return false if user is not able to remove the source branch', () => {
vm.mr.canRemoveSourceBranch = false;
it('should return false if user is not able to remove the source branch', () => {
factory({
...defaultMrProps(),
canRemoveSourceBranch: false,
});
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
});
expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
});
});
describe('statusTextBeforeAuthor', () => {
it('should return "Set by" if the MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
describe('statusTextBeforeAuthor', () => {
it('should return "Set by" if the MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(vm.statusTextBeforeAuthor).toBe('Set by');
});
});
expect(wrapper.findByTestId('beforeStatusText').text()).toBe('Set by');
});
});
describe('statusTextAfterAuthor', () => {
it('should return "to be merged automatically..." if MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
describe('statusTextAfterAuthor', () => {
it('should return "to be merged automatically..." if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(vm.statusTextAfterAuthor).toBe(
'to be merged automatically when the pipeline succeeds',
);
});
});
expect(wrapper.findByTestId('afterStatusText').text()).toBe(
'to be merged automatically when the pipeline succeeds',
);
});
});
describe('cancelButtonText', () => {
it('should return "Cancel automatic merge" if MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
describe('cancelButtonText', () => {
it('should return "Cancel automatic merge" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(vm.cancelButtonText).toBe('Cancel automatic merge');
expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe(
'Cancel automatic merge',
);
});
});
});
});
});
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
it('should set flag and call service then tell main component to update the widget with data', (done) => {
const mrObj = {
is_new_mr_data: true,
};
jest.spyOn(vm.service, 'cancelAutomaticMerge').mockReturnValue(
new Promise((resolve) => {
resolve({
data: mrObj,
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
it('should set flag and call service then tell main component to update the widget with data', (done) => {
factory({
...defaultMrProps(),
});
}),
);
vm.cancelAutomaticMerge();
setImmediate(() => {
expect(vm.isCancellingAutoMerge).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
done();
const mrObj = {
is_new_mr_data: true,
};
jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue(
new Promise((resolve) => {
resolve({
data: mrObj,
});
}),
);
wrapper.vm.cancelAutomaticMerge();
setImmediate(() => {
expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
done();
});
});
});
});
});
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
jest.spyOn(vm.service, 'merge').mockReturnValue(
Promise.resolve({
data: {
status: MWPS_MERGE_STRATEGY,
},
}),
);
vm.removeSourceBranch();
setImmediate(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.merge).toHaveBeenCalledWith({
sha,
auto_merge_strategy: MWPS_MERGE_STRATEGY,
should_remove_source_branch: true,
});
done();
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
factory({
...defaultMrProps(),
});
jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(
Promise.resolve({
data: {
status: MWPS_MERGE_STRATEGY,
},
}),
);
wrapper.vm.removeSourceBranch();
setImmediate(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
sha,
auto_merge_strategy: MWPS_MERGE_STRATEGY,
should_remove_source_branch: true,
});
done();
});
});
});
});
});
});
describe('template', () => {
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds');
describe('template', () => {
it('should have correct elements', () => {
factory({
...defaultMrProps(),
});
expect(vm.$el.innerText).toContain('The changes will be merged into');
expect(vm.$el.innerText).toContain(targetBranch);
expect(vm.$el.innerText).toContain('The source branch will not be deleted');
expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain(
'Cancel automatic merge',
);
expect(wrapper.element).toMatchSnapshot();
});
expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain(
'Delete source branch',
);
it('should disable cancel auto merge button when the action is in progress', async () => {
factory({
...defaultMrProps(),
});
wrapper.setData({
isCancellingAutoMerge: true,
});
expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy();
});
await nextTick();
it('should disable cancel auto merge button when the action is in progress', (done) => {
vm.isCancellingAutoMerge = true;
expect(wrapper.find('.js-cancel-auto-merge').attributes('disabled')).toBe('disabled');
});
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy();
done();
});
});
it('should show source branch will be deleted text when it source branch set to remove', () => {
factory({
...defaultMrProps(),
shouldRemoveSourceBranch: true,
});
it('should show source branch will be deleted text when it source branch set to remove', (done) => {
vm.mr.shouldRemoveSourceBranch = true;
const normalizedText = wrapper.text().replace(/\s+/g, ' ');
Vue.nextTick(() => {
const normalizedText = vm.$el.innerText.replace(/\s+/g, ' ');
expect(normalizedText).toContain('The source branch will be deleted');
expect(normalizedText).not.toContain('The source branch will not be deleted');
});
expect(normalizedText).toContain('The source branch will be deleted');
expect(normalizedText).not.toContain('The source branch will not be deleted');
done();
});
});
it('should not show delete source branch button when user not able to delete source branch', () => {
factory({
...defaultMrProps(),
currentUserId: 4,
});
it('should not show delete source branch button when user not able to delete source branch', (done) => {
vm.mr.currentUserId = 4;
expect(wrapper.find('.js-remove-source-branch').exists()).toBe(false);
});
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-remove-source-branch')).toEqual(null);
done();
});
});
it('should disable delete source branch button when the action is in progress', async () => {
factory({
...defaultMrProps(),
});
wrapper.setData({
isRemovingSourceBranch: true,
});
it('should disable delete source branch button when the action is in progress', (done) => {
vm.isRemovingSourceBranch = true;
await nextTick();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled'),
).toBeTruthy();
done();
});
});
expect(wrapper.find('.js-remove-source-branch').attributes('disabled')).toBe('disabled');
});
it('should render the status text as "...to merged automatically" if MWPS is selected', (done) => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
Vue.nextTick(() => {
const statusText = trimText(vm.$el.querySelector('.js-status-text-after-author').innerText);
const statusText = trimText(wrapper.find('.js-status-text-after-author').text());
expect(statusText).toBe('to be merged automatically when the pipeline succeeds');
done();
});
});
expect(statusText).toBe('to be merged automatically when the pipeline succeeds');
});
it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', (done) => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
Vue.nextTick(() => {
const cancelButtonText = trimText(vm.$el.querySelector('.js-cancel-auto-merge').innerText);
const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
expect(cancelButtonText).toBe('Cancel automatic merge');
done();
expect(cancelButtonText).toBe('Cancel automatic merge');
});
});
});
});
......
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
......@@ -8,43 +9,60 @@ describe('MRWidgetAutoMergeFailed', () => {
const mergeError = 'This is the merge error';
const findButton = () => wrapper.find(GlButton);
const createComponent = (props = {}) => {
const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(AutoMergeFailedComponent, {
propsData: { ...props },
});
};
data() {
if (mergeRequestWidgetGraphql) {
return { mergeError: props.mr?.mergeError };
}
beforeEach(() => {
createComponent({
mr: { mergeError },
return {};
},
provide: {
glFeatures: { mergeRequestWidgetGraphql },
},
});
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders failed message', () => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
});
[true, false].forEach((mergeRequestWidgetGraphql) => {
describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
beforeEach(() => {
createComponent(
{
mr: { mergeError },
},
mergeRequestWidgetGraphql,
);
});
it('renders merge error provided', () => {
expect(wrapper.text()).toContain(mergeError);
});
it('renders failed message', () => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
});
it('render refresh button', () => {
expect(findButton().text()).toEqual('Refresh');
});
it('renders merge error provided', () => {
expect(wrapper.text()).toContain(mergeError);
});
it('render refresh button', () => {
expect(findButton().text()).toBe('Refresh');
});
it('emits event and shows loading icon when button is clicked', async () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
it('emits event and shows loading icon when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
await nextTick();
return wrapper.vm.$nextTick(() => {
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
});
});
......@@ -30,6 +30,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
auto_merge_strategy merge_user
]
if Gitlab.ee?
......
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