Commit c46476c0 authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'ph/235715/autoMergeToGraphql' into 'master'

Moves the auto merge state to GraphQL

See merge request gitlab-org/gitlab!50980
parents ff1e3889 44926ce2
<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
......
......@@ -13836,6 +13836,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
autoMergeEnabled: Boolean!
"""
Selected auto merge strategy
"""
autoMergeStrategy: String
"""
Array of available auto merge strategies
"""
......@@ -14075,6 +14080,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)
"""
......
......@@ -37994,6 +37994,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",
......@@ -38645,6 +38659,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)",
......@@ -2095,6 +2095,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 |
......@@ -2125,6 +2126,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
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
......
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