summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_mr_widget
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_mr_widget')
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js76
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js39
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js44
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js313
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js239
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js70
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js326
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js139
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js85
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js48
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/components/review_app_link_spec.js52
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js31
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js230
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js31
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js69
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js226
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js156
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js219
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js43
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js40
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js26
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js34
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js26
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js19
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js981
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js25
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js99
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js46
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js104
30 files changed, 3841 insertions, 5 deletions
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
new file mode 100644
index 00000000000..f78fcfb52b4
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
@@ -0,0 +1,76 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
+
+describe('MrWidgetAlertMessage', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetAlertMessage), {
+ propsData: {},
+ localVue,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when type is not provided', () => {
+ it('should render a red message', done => {
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('danger_message');
+ expect(wrapper.classes()).not.toContain('warning_message');
+ done();
+ });
+ });
+ });
+
+ describe('when type === "danger"', () => {
+ it('should render a red message', done => {
+ wrapper.setProps({ type: 'danger' });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('danger_message');
+ expect(wrapper.classes()).not.toContain('warning_message');
+ done();
+ });
+ });
+ });
+
+ describe('when type === "warning"', () => {
+ it('should render a red message', done => {
+ wrapper.setProps({ type: 'warning' });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('warning_message');
+ expect(wrapper.classes()).not.toContain('danger_message');
+ done();
+ });
+ });
+ });
+
+ describe('when helpPath is not provided', () => {
+ it('should not render a help icon/link', done => {
+ wrapper.vm.$nextTick(() => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('when helpPath is provided', () => {
+ it('should render a help icon/link', done => {
+ wrapper.setProps({ helpPath: '/path/to/help/docs' });
+ wrapper.vm.$nextTick(() => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toBe('/path/to/help/docs');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js
new file mode 100644
index 00000000000..05690aa1248
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
+
+describe('MrWidgetAuthor', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(MrWidgetAuthor);
+
+ vm = mountComponent(Component, {
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl:
+ 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders link with the author web url', () => {
+ expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root');
+ });
+
+ it('renders image with avatar url', () => {
+ expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(
+ 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ );
+ });
+
+ it('renders author name', () => {
+ expect(vm.$el.textContent.trim()).toEqual('Administrator');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js
new file mode 100644
index 00000000000..58ed92298bf
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
+
+describe('MrWidgetAuthorTime', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(MrWidgetAuthorTime);
+
+ vm = mountComponent(Component, {
+ actionText: 'Merged by',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl:
+ 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ dateTitle: '2017-03-23T23:02:00.807Z',
+ dateReadable: '12 hours ago',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided action text', () => {
+ expect(vm.$el.textContent).toContain('Merged by');
+ });
+
+ it('renders author', () => {
+ expect(vm.$el.textContent).toContain('Administrator');
+ });
+
+ it('renders provided time', () => {
+ expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toEqual(
+ '2017-03-23T23:02:00.807Z',
+ );
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
new file mode 100644
index 00000000000..b492a69fb3d
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
@@ -0,0 +1,313 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
+
+describe('MRWidgetHeader', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(headerComponent);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ gon.relative_url_root = '';
+ });
+
+ const expectDownloadDropdownItems = () => {
+ const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches');
+ const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff');
+
+ expect(downloadEmailPatchesEl.textContent.trim()).toEqual('Email patches');
+ expect(downloadEmailPatchesEl.getAttribute('href')).toEqual('/mr/email-patches');
+ expect(downloadPlainDiffEl.textContent.trim()).toEqual('Plain diff');
+ expect(downloadPlainDiffEl.getAttribute('href')).toEqual('/mr/plainDiffPath');
+ };
+
+ describe('computed', () => {
+ describe('shouldShowCommitsBehindText', () => {
+ it('return true when there are divergedCommitsCount', () => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
+
+ expect(vm.shouldShowCommitsBehindText).toEqual(true);
+ });
+
+ it('returns false where there are no divergedComits count', () => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ statusPath: 'abc',
+ },
+ });
+
+ expect(vm.shouldShowCommitsBehindText).toEqual(false);
+ });
+ });
+
+ describe('commitsBehindText', () => {
+ it('returns singular when there is one commit', () => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 1,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ targetBranchPath: '/foo/bar/master',
+ statusPath: 'abc',
+ },
+ });
+
+ expect(vm.commitsBehindText).toEqual(
+ 'The source branch is <a href="/foo/bar/master">1 commit behind</a> the target branch',
+ );
+ });
+
+ it('returns plural when there is more than one commit', () => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 2,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ targetBranchPath: '/foo/bar/master',
+ statusPath: 'abc',
+ },
+ });
+
+ expect(vm.commitsBehindText).toEqual(
+ 'The source branch is <a href="/foo/bar/master">2 commits behind</a> the target branch',
+ );
+ });
+ });
+ });
+
+ describe('template', () => {
+ describe('common elements', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
+ });
+
+ it('renders source branch link', () => {
+ expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
+ '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ );
+ });
+
+ it('renders clipboard button', () => {
+ expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null);
+ });
+
+ it('renders target branch', () => {
+ expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master');
+ });
+ });
+
+ describe('with an open merge request', () => {
+ const mrDefaultOptions = {
+ iid: 1,
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ canPushToSourceBranch: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ sourceProjectFullPath: 'root/gitlab-ce',
+ targetProjectFullPath: 'gitlab-org/gitlab-ce',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: { ...mrDefaultOptions },
+ });
+ });
+
+ it('renders checkout branch button with modal trigger', () => {
+ const button = vm.$el.querySelector('.js-check-out-branch');
+
+ expect(button.textContent.trim()).toEqual('Check out branch');
+ expect(button.getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(button.getAttribute('data-toggle')).toEqual('modal');
+ });
+
+ it('renders web ide button', () => {
+ const button = vm.$el.querySelector('.js-web-ide');
+
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
+ expect(button.classList.contains('disabled')).toBe(false);
+ expect(button.getAttribute('href')).toEqual(
+ '/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=gitlab-org%2Fgitlab-ce',
+ );
+ });
+
+ it('renders web ide button in disabled state with no href', () => {
+ const mr = { ...mrDefaultOptions, canPushToSourceBranch: false };
+ vm = mountComponent(Component, { mr });
+
+ const link = vm.$el.querySelector('.js-web-ide');
+
+ expect(link.classList.contains('disabled')).toBe(true);
+ expect(link.getAttribute('href')).toBeNull();
+ });
+
+ it('renders web ide button with blank query string if target & source project branch', done => {
+ vm.mr.targetProjectFullPath = 'root/gitlab-ce';
+
+ vm.$nextTick(() => {
+ const button = vm.$el.querySelector('.js-web-ide');
+
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
+ expect(button.getAttribute('href')).toEqual(
+ '/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=',
+ );
+
+ done();
+ });
+ });
+
+ it('renders web ide button with relative URL', done => {
+ gon.relative_url_root = '/gitlab';
+ vm.mr.iid = 2;
+
+ vm.$nextTick(() => {
+ const button = vm.$el.querySelector('.js-web-ide');
+
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
+ expect(button.getAttribute('href')).toEqual(
+ '/gitlab/-/ide/project/root/gitlab-ce/merge_requests/2?target_project=gitlab-org%2Fgitlab-ce',
+ );
+
+ done();
+ });
+ });
+
+ it('renders download dropdown with links', () => {
+ expectDownloadDropdownItems();
+ });
+ });
+
+ describe('with a closed merge request', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: false,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
+ });
+
+ it('does not render checkout branch button with modal trigger', () => {
+ const button = vm.$el.querySelector('.js-check-out-branch');
+
+ expect(button).toEqual(null);
+ });
+
+ it('renders download dropdown with links', () => {
+ expectDownloadDropdownItems();
+ });
+ });
+
+ describe('without diverged commits', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
+ });
+
+ it('does not render diverged commits info', () => {
+ expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null);
+ });
+ });
+
+ describe('with diverged commits', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ statusPath: 'abc',
+ },
+ });
+ });
+
+ it('renders diverged commits info', () => {
+ expect(vm.$el.querySelector('.diverged-commits-count').textContent).toEqual(
+ 'The source branch is 12 commits behind the target branch',
+ );
+
+ expect(vm.$el.querySelector('.diverged-commits-count a').textContent).toEqual(
+ '12 commits behind',
+ );
+
+ expect(vm.$el.querySelector('.diverged-commits-count a')).toHaveAttr(
+ 'href',
+ vm.mr.targetBranchPath,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
new file mode 100644
index 00000000000..7a932feb3a7
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -0,0 +1,239 @@
+import Vue from 'vue';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+
+const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
+const monitoringUrl = '/root/acets-review-apps/environments/15/metrics';
+
+const metricsMockData = {
+ success: true,
+ metrics: {
+ memory_before: [
+ {
+ metric: {},
+ value: [1495785220.607, '9572875.906976745'],
+ },
+ ],
+ memory_after: [
+ {
+ metric: {},
+ value: [1495787020.607, '4485853.130206379'],
+ },
+ ],
+ memory_values: [
+ {
+ metric: {},
+ values: [[1493716685, '4.30859375']],
+ },
+ ],
+ },
+ last_update: '2017-05-02T12:34:49.628Z',
+ deployment_time: 1493718485,
+};
+
+const createComponent = () => {
+ const Component = Vue.extend(MemoryUsage);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: {
+ metricsUrl: url,
+ metricsMonitoringUrl: monitoringUrl,
+ memoryMetrics: [],
+ deploymentTime: 0,
+ hasMetrics: false,
+ loadFailed: false,
+ loadingMetrics: true,
+ backOffRequestCounter: 0,
+ },
+ });
+};
+
+const messages = {
+ loadingMetrics: 'Loading deployment statistics',
+ hasMetrics: 'Memory usage is unchanged at 0MB',
+ loadFailed: 'Failed to load deployment statistics',
+ metricsUnavailable: 'Deployment statistics are not available currently',
+};
+
+describe('MemoryUsage', () => {
+ let vm;
+ let el;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(`${url}.json`).reply(200);
+
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const data = MemoryUsage.data();
+
+ expect(Array.isArray(data.memoryMetrics)).toBeTruthy();
+ expect(data.memoryMetrics.length).toBe(0);
+
+ expect(typeof data.deploymentTime).toBe('number');
+ expect(data.deploymentTime).toBe(0);
+
+ expect(typeof data.hasMetrics).toBe('boolean');
+ expect(data.hasMetrics).toBeFalsy();
+
+ expect(typeof data.loadFailed).toBe('boolean');
+ expect(data.loadFailed).toBeFalsy();
+
+ expect(typeof data.loadingMetrics).toBe('boolean');
+ expect(data.loadingMetrics).toBeTruthy();
+
+ expect(typeof data.backOffRequestCounter).toBe('number');
+ expect(data.backOffRequestCounter).toBe(0);
+ });
+ });
+
+ describe('computed', () => {
+ describe('memoryChangeMessage', () => {
+ it('should contain "increased" if memoryFrom value is less than memoryTo value', () => {
+ vm.memoryFrom = 4.28;
+ vm.memoryTo = 9.13;
+
+ expect(vm.memoryChangeMessage.indexOf('increased')).not.toEqual('-1');
+ });
+
+ it('should contain "decreased" if memoryFrom value is less than memoryTo value', () => {
+ vm.memoryFrom = 9.13;
+ vm.memoryTo = 4.28;
+
+ expect(vm.memoryChangeMessage.indexOf('decreased')).not.toEqual('-1');
+ });
+
+ it('should contain "unchanged" if memoryFrom value equal to memoryTo value', () => {
+ vm.memoryFrom = 1;
+ vm.memoryTo = 1;
+
+ expect(vm.memoryChangeMessage.indexOf('unchanged')).not.toEqual('-1');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ const { metrics, deployment_time } = metricsMockData;
+
+ describe('getMegabytes', () => {
+ it('should return Megabytes from provided Bytes value', () => {
+ const memoryInBytes = '9572875.906976745';
+
+ expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13');
+ });
+ });
+
+ describe('computeGraphData', () => {
+ it('should populate sparkline graph', () => {
+ // ignore BoostrapVue warnings
+ jest.spyOn(console, 'warn').mockImplementation();
+
+ vm.computeGraphData(metrics, deployment_time);
+ const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
+
+ expect(hasMetrics).toBeTruthy();
+ expect(memoryMetrics.length).toBeGreaterThan(0);
+ expect(deploymentTime).toEqual(deployment_time);
+ expect(memoryFrom).toEqual('9.13');
+ expect(memoryTo).toEqual('4.28');
+ });
+ });
+
+ describe('loadMetrics', () => {
+ const returnServicePromise = () =>
+ new Promise(resolve => {
+ resolve({
+ data: metricsMockData,
+ });
+ });
+
+ it('should load metrics data using MRWidgetService', done => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockReturnValue(returnServicePromise(true));
+ jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {});
+
+ vm.loadMetrics();
+ setImmediate(() => {
+ expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
+ expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-memory-usage')).toBeTruthy();
+ expect(el.querySelector('.js-usage-info')).toBeDefined();
+ });
+
+ it('should show loading metrics message while metrics are being loaded', done => {
+ vm.loadingMetrics = true;
+ vm.hasMetrics = false;
+ vm.loadFailed = false;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
+
+ expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
+
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
+ done();
+ });
+ });
+
+ it('should show deployment memory usage when metrics are loaded', done => {
+ // ignore BoostrapVue warnings
+ jest.spyOn(console, 'warn').mockImplementation();
+
+ vm.loadingMetrics = false;
+ vm.hasMetrics = true;
+ vm.loadFailed = false;
+ vm.memoryMetrics = metricsMockData.metrics.memory_values[0].values;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.memory-graph-container')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
+ done();
+ });
+ });
+
+ it('should show failure message when metrics loading failed', done => {
+ vm.loadingMetrics = false;
+ vm.hasMetrics = false;
+ vm.loadFailed = true;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
+
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
+ done();
+ });
+ });
+
+ it('should show metrics unavailable message when metrics loading failed', done => {
+ vm.loadingMetrics = false;
+ vm.hasMetrics = false;
+ vm.loadFailed = false;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
+
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js
new file mode 100644
index 00000000000..00e79a22485
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
+
+describe('MRWidgetMergeHelp', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(mergeHelpComponent);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with missing branch', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ missingBranch: 'this-is-not-the-branch-you-are-looking-for',
+ });
+ });
+
+ it('renders missing branch information', () => {
+ expect(
+ vm.$el.textContent
+ .trim()
+ .replace(/[\r\n]+/g, ' ')
+ .replace(/\s\s+/g, ' '),
+ ).toEqual(
+ 'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line',
+ );
+ });
+
+ it('renders button to open help modal', () => {
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual(
+ '#modal_merge_info',
+ );
+
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual(
+ 'modal',
+ );
+ });
+ });
+
+ describe('without missing branch', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('renders information about how to merge manually', () => {
+ expect(
+ vm.$el.textContent
+ .trim()
+ .replace(/[\r\n]+/g, ' ')
+ .replace(/\s\s+/g, ' '),
+ ).toEqual('You can merge this merge request manually using the command line');
+ });
+
+ it('renders element to open a modal', () => {
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual(
+ '#modal_merge_info',
+ );
+
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual(
+ 'modal',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
new file mode 100644
index 00000000000..309aec179d9
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -0,0 +1,326 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { trimText } from 'helpers/text_helper';
+import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import mockData from '../mock_data';
+
+describe('MRWidgetPipeline', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(pipelineComponent);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hasPipeline', () => {
+ it('should return true when there is a pipeline', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ ciStatus: 'success',
+ hasCi: true,
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.hasPipeline).toEqual(true);
+ });
+
+ it('should return false when there is no pipeline', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.hasPipeline).toEqual(false);
+ });
+ });
+
+ describe('hasCIError', () => {
+ it('should return false when there is no CI error', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.hasCIError).toEqual(false);
+ });
+
+ it('should return true when there is a CI error', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: null,
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.hasCIError).toEqual(true);
+ });
+ });
+
+ describe('coverageDeltaClass', () => {
+ it('should return no class if there is no coverage change', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ pipelineCoverageDelta: '0',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.coverageDeltaClass).toEqual('');
+ });
+
+ it('should return text-success if the coverage increased', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ pipelineCoverageDelta: '10',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.coverageDeltaClass).toEqual('text-success');
+ });
+
+ it('should return text-danger if the coverage decreased', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ pipelineCoverageDelta: '-12',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.coverageDeltaClass).toEqual('text-danger');
+ });
+ });
+ });
+
+ describe('rendered output', () => {
+ it('should render CI error', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
+ );
+ });
+
+ it('should render CI error when no pipeline is provided', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
+ );
+ });
+
+ it('should render CI error when no CI is provided and pipeline must succeed is turned on', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ hasCi: false,
+ pipelineMustSucceed: true,
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'No pipeline has been run for this commit.',
+ );
+ });
+
+ describe('with a pipeline', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ pipelineCoverageDelta: mockData.pipelineCoverageDelta,
+ troubleshootingDocsPath: 'help',
+ });
+ });
+
+ it('should render pipeline ID', () => {
+ expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
+ `#${mockData.pipeline.id}`,
+ );
+ });
+
+ it('should render pipeline status and commit id', () => {
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ mockData.pipeline.details.status.label,
+ );
+
+ expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual(
+ mockData.pipeline.commit.short_id,
+ );
+
+ expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual(
+ mockData.pipeline.commit.commit_path,
+ );
+ });
+
+ it('should render pipeline graph', () => {
+ expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
+ mockData.pipeline.details.stages.length,
+ );
+ });
+
+ it('should render coverage information', () => {
+ expect(vm.$el.querySelector('.media-body').textContent).toContain(
+ `Coverage ${mockData.pipeline.coverage}`,
+ );
+ });
+
+ it('should render pipeline coverage delta information', () => {
+ expect(vm.$el.querySelector('.js-pipeline-coverage-delta.text-danger')).toBeDefined();
+ expect(vm.$el.querySelector('.js-pipeline-coverage-delta').textContent).toContain(
+ `(${mockData.pipelineCoverageDelta}%)`,
+ );
+ });
+ });
+
+ describe('without commit path', () => {
+ beforeEach(() => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.commit;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+ });
+
+ it('should render pipeline ID', () => {
+ expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
+ `#${mockData.pipeline.id}`,
+ );
+ });
+
+ it('should render pipeline status', () => {
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ mockData.pipeline.details.status.label,
+ );
+
+ expect(vm.$el.querySelector('.js-commit-link')).toBeNull();
+ });
+
+ it('should render pipeline graph', () => {
+ expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
+ mockData.pipeline.details.stages.length,
+ );
+ });
+
+ it('should render coverage information', () => {
+ expect(vm.$el.querySelector('.media-body').textContent).toContain(
+ `Coverage ${mockData.pipeline.coverage}`,
+ );
+ });
+ });
+
+ describe('without coverage', () => {
+ it('should not render a coverage', () => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.coverage;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage');
+ });
+ });
+
+ describe('without a pipeline graph', () => {
+ it('should not render a pipeline graph', () => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.details.stages;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
+ });
+ });
+
+ describe('for each type of pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ ({ pipeline } = JSON.parse(JSON.stringify(mockData)));
+
+ pipeline.details.name = 'Pipeline';
+ pipeline.merge_request_event_type = undefined;
+ pipeline.ref.tag = false;
+ pipeline.ref.branch = false;
+ });
+
+ const factory = () => {
+ vm = mountComponent(Component, {
+ pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ sourceBranchLink: mockData.source_branch_link,
+ });
+ };
+
+ describe('for a branch pipeline', () => {
+ it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
+ pipeline.ref.branch = true;
+
+ factory();
+
+ const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('for a tag pipeline', () => {
+ it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
+ pipeline.ref.tag = true;
+
+ factory();
+
+ const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('for a detached merge request pipeline', () => {
+ it('renders a pipeline widget that reads "Detached merge request pipeline <ID> <status> for <SHA>"', () => {
+ pipeline.details.name = 'Detached merge request pipeline';
+ pipeline.merge_request_event_type = 'detached';
+
+ factory();
+
+ const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
new file mode 100644
index 00000000000..6ec30493f8b
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -0,0 +1,139 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
+
+describe('Merge request widget rebase component', () => {
+ let Component;
+ let vm;
+ beforeEach(() => {
+ Component = Vue.extend(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('While rebasing', () => {
+ it('should show progress message', () => {
+ vm = mountComponent(Component, {
+ mr: { rebaseInProgress: true },
+ service: {},
+ });
+
+ expect(
+ vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(),
+ ).toContain('Rebase in progress');
+ });
+ });
+
+ describe('With permissions', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {},
+ });
+ });
+
+ it('it should render rebase button and warning message', () => {
+ const text = vm.$el
+ .querySelector('.rebase-state-find-class-convention span')
+ .textContent.trim();
+
+ expect(text).toContain('Fast-forward merge is not possible.');
+ expect(text.replace(/\s\s+/g, ' ')).toContain(
+ 'Rebase the source branch onto the target branch.',
+ );
+ });
+
+ it('it should render error message when it fails', done => {
+ vm.rebasingError = 'Something went wrong!';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(),
+ ).toContain('Something went wrong!');
+ done();
+ });
+ });
+ });
+
+ describe('Without permissions', () => {
+ it('should render a message explaining user does not have permissions', () => {
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: 'foo',
+ },
+ service: {},
+ });
+
+ const text = vm.$el
+ .querySelector('.rebase-state-find-class-convention span')
+ .textContent.trim();
+
+ expect(text).toContain('Fast-forward merge is not possible.');
+ expect(text).toContain('Rebase the source branch onto');
+ expect(text).toContain('foo');
+ expect(text.replace(/\s\s+/g, ' ')).toContain('to allow this merge request to be merged.');
+ });
+
+ it('should render the correct target branch name', () => {
+ const targetBranch = 'fake-branch-to-test-with';
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch,
+ },
+ service: {},
+ });
+
+ const elem = vm.$el.querySelector('.rebase-state-find-class-convention span');
+
+ expect(elem.innerHTML).toContain(
+ `Fast-forward merge is not possible. Rebase the source branch onto <span class="label-branch">${targetBranch}</span> to allow this merge request to be merged.`,
+ );
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', done => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ vm = mountComponent(Component, {
+ mr: {},
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ merge_error: null,
+ },
+ });
+ },
+ },
+ });
+
+ vm.rebase();
+
+ // Wait for the rebase request
+ vm.$nextTick()
+ // Wait for the polling request
+ .then(vm.$nextTick())
+ // Wait for the eventHub to be called
+ .then(vm.$nextTick())
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js
new file mode 100644
index 00000000000..0c4ec7ed99b
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue';
+
+describe('MRWidgetRelatedLinks', () => {
+ let vm;
+
+ const createComponent = data => {
+ const Component = Vue.extend(relatedLinksComponent);
+
+ return mountComponent(Component, data);
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('closesText', () => {
+ it('returns Closes text for open merge request', () => {
+ vm = createComponent({ state: 'open', relatedLinks: {} });
+
+ expect(vm.closesText).toEqual('Closes');
+ });
+
+ it('returns correct text for closed merge request', () => {
+ vm = createComponent({ state: 'closed', relatedLinks: {} });
+
+ expect(vm.closesText).toEqual('Did not close');
+ });
+
+ it('returns correct tense for merged request', () => {
+ vm = createComponent({ state: 'merged', relatedLinks: {} });
+
+ expect(vm.closesText).toEqual('Closed');
+ });
+ });
+ });
+
+ it('should have only have closing issues text', () => {
+ vm = createComponent({
+ relatedLinks: {
+ closing: '<a href="#">#23</a> and <a>#42</a>',
+ },
+ });
+ const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
+
+ expect(content).toContain('Closes #23 and #42');
+ expect(content).not.toContain('Mentions');
+ });
+
+ it('should have only have mentioned issues text', () => {
+ vm = createComponent({
+ relatedLinks: {
+ mentioned: '<a href="#">#7</a>',
+ },
+ });
+
+ expect(vm.$el.innerText).toContain('Mentions #7');
+ expect(vm.$el.innerText).not.toContain('Closes');
+ });
+
+ it('should have closing and mentioned issues at the same time', () => {
+ vm = createComponent({
+ relatedLinks: {
+ closing: '<a href="#">#7</a>',
+ mentioned: '<a href="#">#23</a> and <a>#42</a>',
+ },
+ });
+ const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
+
+ expect(content).toContain('Closes #7');
+ expect(content).toContain('Mentions #23 and #42');
+ });
+
+ it('should have assing issues link', () => {
+ vm = createComponent({
+ relatedLinks: {
+ assignToMe: '<a href="#">Assign yourself to these issues</a>',
+ },
+ });
+
+ expect(vm.$el.innerText).toContain('Assign yourself to these issues');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
new file mode 100644
index 00000000000..6c3b4a01659
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+
+describe('MR widget status icon component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(mrStatusIcon);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('while loading', () => {
+ it('renders loading icon', () => {
+ vm = mountComponent(Component, { status: 'loading' });
+
+ expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner');
+ });
+ });
+
+ describe('with status icon', () => {
+ it('renders ci status icon', () => {
+ vm = mountComponent(Component, { status: 'failed' });
+
+ expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull();
+ });
+ });
+
+ describe('with disabled button', () => {
+ it('renders a disabled button', () => {
+ vm = mountComponent(Component, { status: 'failed', showDisabledButton: true });
+
+ expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge');
+ });
+ });
+
+ describe('without disabled button', () => {
+ it('does not render a disabled button', () => {
+ vm = mountComponent(Component, { status: 'failed' });
+
+ expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js
index 91e95b2bdb1..62c5c8e8531 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
@@ -38,7 +38,7 @@ describe('MrWidgetTerraformPlan', () => {
describe('loading poll', () => {
beforeEach(() => {
- mockPollingApi(200, { 'tfplan.json': plan }, {});
+ mockPollingApi(200, { '123': plan }, {});
return mountWrapper().then(() => {
wrapper.setData({ loading: true });
@@ -65,7 +65,7 @@ describe('MrWidgetTerraformPlan', () => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
pollStop = jest.spyOn(Poll.prototype, 'stop');
- mockPollingApi(200, { 'tfplan.json': plan }, {});
+ mockPollingApi(200, { '123': plan }, {});
return mountWrapper();
});
@@ -80,7 +80,7 @@ describe('MrWidgetTerraformPlan', () => {
});
it('renders button when url is found', () => {
- expect(wrapper.find('a').text()).toContain('View full log');
+ expect(wrapper.find(GlLink).exists()).toBe(true);
});
it('does not make additional requests after poll is successful', () => {
@@ -101,7 +101,7 @@ describe('MrWidgetTerraformPlan', () => {
);
expect(wrapper.find('.js-terraform-report-link').exists()).toBe(false);
- expect(wrapper.text()).not.toContain('View full log');
+ expect(wrapper.find(GlLink).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js b/spec/frontend/vue_mr_widget/components/review_app_link_spec.js
new file mode 100644
index 00000000000..7b063653a93
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/review_app_link_spec.js
@@ -0,0 +1,52 @@
+import Vue from 'vue';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import component from '~/vue_merge_request_widget/components/review_app_link.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('review app link', () => {
+ const Component = Vue.extend(component);
+ const props = {
+ link: '/review',
+ cssClass: 'js-link',
+ display: {
+ text: 'View app',
+ tooltip: '',
+ },
+ };
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ el = vm.$el;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided link as href attribute', () => {
+ expect(el.getAttribute('href')).toEqual(props.link);
+ });
+
+ it('renders provided cssClass as class attribute', () => {
+ expect(el.getAttribute('class')).toEqual(props.cssClass);
+ });
+
+ it('renders View app text', () => {
+ expect(el.textContent.trim()).toEqual('View app');
+ });
+
+ it('renders svg icon', () => {
+ expect(el.querySelector('svg')).not.toBeNull();
+ });
+
+ it('tracks an event when clicked', () => {
+ const spy = mockTracking('_category_', el, jest.spyOn);
+ triggerEvent(el);
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
+ label: 'review_app',
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js
new file mode 100644
index 00000000000..4bdc6c95f22
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
+
+describe('MRWidgetArchived', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(archivedComponent);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a ci status failed icon', () => {
+ expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull();
+ });
+
+ it('renders a disabled button', () => {
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
+ expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Merge');
+ });
+
+ it('renders information', () => {
+ expect(vm.$el.querySelector('.bold').textContent.trim()).toEqual(
+ 'This project is archived, write access has been disabled',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
new file mode 100644
index 00000000000..e2caa6e8092
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -0,0 +1,230 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { trimText } from 'helpers/text_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';
+
+describe('MRWidgetAutoMergeEnabled', () => {
+ let vm;
+ const targetBranchPath = '/foo/bar';
+ const targetBranch = 'foo';
+ const sha = '1EA2EZ34';
+
+ beforeEach(() => {
+ const Component = Vue.extend(autoMergeEnabledComponent);
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ 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();
+ });
+
+ describe('computed', () => {
+ describe('canRemoveSourceBranch', () => {
+ it('should return true when user is able to remove source branch', () => {
+ expect(vm.canRemoveSourceBranch).toBeTruthy();
+ });
+
+ 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();
+
+ vm.mr.currentUserId = 3;
+
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+ });
+
+ it('should return false when shouldRemoveSourceBranch set to false', () => {
+ vm.mr.shouldRemoveSourceBranch = true;
+
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+ });
+
+ it('should return false if user is not able to remove the source branch', () => {
+ vm.mr.canRemoveSourceBranch = false;
+
+ expect(vm.canRemoveSourceBranch).toBeFalsy();
+ });
+ });
+
+ describe('statusTextBeforeAuthor', () => {
+ it('should return "Set by" if the MWPS is selected', () => {
+ Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
+
+ expect(vm.statusTextBeforeAuthor).toBe('Set by');
+ });
+ });
+
+ describe('statusTextAfterAuthor', () => {
+ it('should return "to be merged automatically..." if MWPS is selected', () => {
+ Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
+
+ expect(vm.statusTextAfterAuthor).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);
+
+ expect(vm.cancelButtonText).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,
+ });
+ }),
+ );
+
+ vm.cancelAutomaticMerge();
+ setImmediate(() => {
+ expect(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('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');
+
+ 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(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
+ expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain(
+ 'Delete source branch',
+ );
+
+ expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy();
+ });
+
+ it('should disable cancel auto merge button when the action is in progress', done => {
+ vm.isCancellingAutoMerge = true;
+
+ 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', done => {
+ vm.mr.shouldRemoveSourceBranch = true;
+
+ 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');
+ done();
+ });
+ });
+
+ it('should not show delete source branch button when user not able to delete source branch', done => {
+ vm.mr.currentUserId = 4;
+
+ 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', done => {
+ vm.isRemovingSourceBranch = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled'),
+ ).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should render the status text as "...to merged automatically" if MWPS is selected', done => {
+ Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
+
+ Vue.nextTick(() => {
+ const statusText = trimText(vm.$el.querySelector('.js-status-text-after-author').innerText);
+
+ expect(statusText).toBe('to be merged automatically when the pipeline succeeds');
+ done();
+ });
+ });
+
+ it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', done => {
+ Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
+
+ Vue.nextTick(() => {
+ const cancelButtonText = trimText(vm.$el.querySelector('.js-cancel-auto-merge').innerText);
+
+ expect(cancelButtonText).toBe('Cancel automatic merge');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js
new file mode 100644
index 00000000000..56d55c9afac
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
+
+describe('MRWidgetChecking', () => {
+ let Component;
+ let vm;
+
+ beforeEach(() => {
+ Component = Vue.extend(checkingComponent);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
+ });
+
+ it('renders loading icon', () => {
+ expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner');
+ });
+
+ it('renders information about merging', () => {
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual(
+ 'Checking ability to merge automatically…',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js
new file mode 100644
index 00000000000..322f440763c
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
+
+describe('MRWidgetClosed', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(closedComponent);
+ vm = mountComponent(Component, {
+ mr: {
+ metrics: {
+ mergedBy: {},
+ closedBy: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl:
+ 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ readableMergedAt: '',
+ readableClosedAt: 'less than a minute ago',
+ },
+ targetBranchPath: '/twitter/flight/commits/so_long_jquery',
+ targetBranch: 'so_long_jquery',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders warning icon', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
+ });
+
+ it('renders closed by information with author and time', () => {
+ expect(
+ vm.$el
+ .querySelector('.js-mr-widget-author')
+ .textContent.trim()
+ .replace(/\s\s+/g, ' '),
+ ).toContain('Closed by Administrator less than a minute ago');
+ });
+
+ it('links to the user that closed the MR', () => {
+ expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual(
+ 'http://localhost:3000/root',
+ );
+ });
+
+ it('renders information about the changes not being merged', () => {
+ expect(
+ vm.$el
+ .querySelector('.mr-info-list')
+ .textContent.trim()
+ .replace(/\s\s+/g, ' '),
+ ).toContain('The changes were not merged into so_long_jquery');
+ });
+
+ it('renders link for target branch', () => {
+ expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual(
+ '/twitter/flight/commits/so_long_jquery',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
new file mode 100644
index 00000000000..d3482b457ad
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -0,0 +1,226 @@
+import $ from 'jquery';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { removeBreakLine } from 'helpers/text_helper';
+import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('MRWidgetConflicts', () => {
+ let vm;
+ const path = '/conflicts';
+
+ function createComponent(propsData = {}) {
+ const localVue = createLocalVue();
+
+ vm = shallowMount(localVue.extend(ConflictsComponent), {
+ propsData,
+ });
+ }
+
+ beforeEach(() => {
+ jest.spyOn($.fn, 'popover');
+ });
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ // There are two permissions we need to consider:
+ //
+ // 1. Is the user allowed to merge to the target branch?
+ // 2. Is the user allowed to push to the source branch?
+ //
+ // This yields 4 possible permutations that we need to test, and
+ // we test them below. A user who can push to the source
+ // branch should be allowed to resolve conflicts. This is
+ // consistent with what the backend does.
+ describe('when allowed to merge but not allowed to push to source branch', () => {
+ beforeEach(() => {
+ createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: false,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
+ });
+ });
+
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(vm.text()).toContain('There are merge conflicts');
+ expect(vm.text()).not.toContain('ask someone with write access');
+ });
+
+ it('should not allow you to resolve the conflicts', () => {
+ expect(vm.text()).not.toContain('Resolve conflicts');
+ });
+
+ it('should have merge buttons', () => {
+ const mergeLocallyButton = vm.find('.js-merge-locally-button');
+
+ expect(mergeLocallyButton.text()).toContain('Merge locally');
+ });
+ });
+
+ describe('when not allowed to merge but allowed to push to source branch', () => {
+ beforeEach(() => {
+ createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
+ });
+ });
+
+ it('should tell you about conflicts', () => {
+ expect(vm.text()).toContain('There are merge conflicts');
+ expect(vm.text()).toContain('ask someone with write access');
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ const resolveButton = vm.find('.js-resolve-conflicts-button');
+
+ expect(resolveButton.text()).toContain('Resolve conflicts');
+ expect(resolveButton.attributes('href')).toEqual(path);
+ });
+
+ it('should not have merge buttons', () => {
+ expect(vm.text()).not.toContain('Merge locally');
+ });
+ });
+
+ describe('when allowed to merge and push to source branch', () => {
+ beforeEach(() => {
+ createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
+ });
+ });
+
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(vm.text()).toContain('There are merge conflicts');
+ expect(vm.text()).not.toContain('ask someone with write access');
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ const resolveButton = vm.find('.js-resolve-conflicts-button');
+
+ expect(resolveButton.text()).toContain('Resolve conflicts');
+ expect(resolveButton.attributes('href')).toEqual(path);
+ });
+
+ it('should have merge buttons', () => {
+ const mergeLocallyButton = vm.find('.js-merge-locally-button');
+
+ expect(mergeLocallyButton.text()).toContain('Merge locally');
+ });
+ });
+
+ describe('when user does not have permission to push to source branch', () => {
+ it('should show proper message', () => {
+ createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: false,
+ conflictsDocsPath: '',
+ },
+ });
+
+ expect(
+ vm
+ .text()
+ .trim()
+ .replace(/\s\s+/g, ' '),
+ ).toContain('ask someone with write access');
+ });
+
+ it('should not have action buttons', () => {
+ createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: false,
+ conflictsDocsPath: '',
+ },
+ });
+
+ expect(vm.contains('.js-resolve-conflicts-button')).toBe(false);
+ expect(vm.contains('.js-merge-locally-button')).toBe(false);
+ });
+
+ it('should not have resolve button when no conflict resolution path', () => {
+ createComponent({
+ mr: {
+ canMerge: true,
+ conflictResolutionPath: null,
+ conflictsDocsPath: '',
+ },
+ });
+
+ expect(vm.contains('.js-resolve-conflicts-button')).toBe(false);
+ });
+ });
+
+ describe('when fast-forward or semi-linear merge enabled', () => {
+ it('should tell you to rebase locally', () => {
+ createComponent({
+ mr: {
+ shouldBeRebased: true,
+ conflictsDocsPath: '',
+ },
+ });
+
+ expect(removeBreakLine(vm.text()).trim()).toContain(
+ 'Fast-forward merge is not possible. To merge this request, first rebase locally.',
+ );
+ });
+ });
+
+ describe('when source branch protected', () => {
+ beforeEach(() => {
+ createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: TEST_HOST,
+ sourceBranchProtected: true,
+ conflictsDocsPath: '',
+ },
+ });
+ });
+
+ it('sets resolve button as disabled', () => {
+ expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('disabled');
+ });
+
+ it('renders popover', () => {
+ expect($.fn.popover).toHaveBeenCalled();
+ });
+ });
+
+ describe('when source branch not protected', () => {
+ beforeEach(() => {
+ createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: TEST_HOST,
+ sourceBranchProtected: false,
+ conflictsDocsPath: '',
+ },
+ });
+ });
+
+ it('sets resolve button as disabled', () => {
+ expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe(undefined);
+ });
+
+ it('renders popover', () => {
+ expect($.fn.popover).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
new file mode 100644
index 00000000000..f591393d721
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -0,0 +1,156 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+
+describe('MRWidgetFailedToMerge', () => {
+ const dummyIntervalId = 1337;
+ let Component;
+ let mr;
+ let vm;
+
+ beforeEach(() => {
+ Component = Vue.extend(failedToMergeComponent);
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(window, 'setInterval').mockReturnValue(dummyIntervalId);
+ jest.spyOn(window, 'clearInterval').mockImplementation();
+ mr = {
+ mergeError: 'Merge error happened',
+ };
+ vm = mountComponent(Component, {
+ mr,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('sets interval to refresh', () => {
+ expect(window.setInterval).toHaveBeenCalledWith(vm.updateTimer, 1000);
+ expect(vm.intervalId).toBe(dummyIntervalId);
+ });
+
+ it('clears interval when destroying ', () => {
+ vm.$destroy();
+
+ expect(window.clearInterval).toHaveBeenCalledWith(dummyIntervalId);
+ });
+
+ describe('computed', () => {
+ describe('timerText', () => {
+ it('should return correct timer text', () => {
+ expect(vm.timerText).toEqual('Refreshing in 10 seconds to show the updated status...');
+
+ vm.timer = 1;
+
+ expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...');
+ });
+ });
+
+ describe('mergeError', () => {
+ it('removes forced line breaks', done => {
+ mr.mergeError = 'contains<br />line breaks<br />';
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.mergeError).toBe('contains line breaks');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should disable polling', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling');
+ });
+ });
+
+ describe('methods', () => {
+ describe('refresh', () => {
+ it('should emit event to request component refresh', () => {
+ expect(vm.isRefreshing).toEqual(false);
+
+ vm.refresh();
+
+ expect(vm.isRefreshing).toEqual(true);
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling');
+ });
+ });
+
+ describe('updateTimer', () => {
+ it('should update timer and emit event when timer end', () => {
+ jest.spyOn(vm, 'refresh').mockImplementation(() => {});
+
+ expect(vm.timer).toEqual(10);
+
+ for (let i = 0; i < 10; i += 1) {
+ expect(vm.timer).toEqual(10 - i);
+ vm.updateTimer();
+ }
+
+ expect(vm.refresh).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('while it is refreshing', () => {
+ it('renders Refresing now', done => {
+ vm.isRefreshing = true;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual(
+ 'Refreshing now',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('while it is not regresing', () => {
+ it('renders warning icon and disabled merge button', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
+ });
+
+ it('renders given error', () => {
+ expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
+ 'Merge error happened',
+ );
+ });
+
+ it('renders refresh button', () => {
+ expect(vm.$el.querySelector('.js-refresh-button').textContent.trim()).toEqual('Refresh now');
+ });
+
+ it('renders remaining time', () => {
+ expect(vm.$el.querySelector('.has-custom-error').textContent.trim()).toEqual(
+ 'Refreshing in 10 seconds to show the updated status...',
+ );
+ });
+ });
+
+ it('should just generic merge failed message if merge_error is not available', done => {
+ vm.mr.mergeError = null;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('Merge failed.');
+ expect(vm.$el.innerText).not.toContain('Merge error happened.');
+ done();
+ });
+ });
+
+ it('should show refresh label when refresh requested', done => {
+ vm.refresh();
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing');
+ expect(vm.$el.innerText).toContain('Refreshing now');
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
new file mode 100644
index 00000000000..1921599ae95
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -0,0 +1,219 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+
+describe('MRWidgetMerged', () => {
+ let vm;
+ const targetBranch = 'foo';
+ const selectors = {
+ get copyMergeShaButton() {
+ return vm.$el.querySelector('button.js-mr-merged-copy-sha');
+ },
+ get mergeCommitShaLink() {
+ return vm.$el.querySelector('a.js-mr-merged-commit-sha');
+ },
+ };
+
+ beforeEach(() => {
+ const Component = Vue.extend(mergedComponent);
+ const mr = {
+ isRemovingSourceBranch: false,
+ cherryPickInForkPath: false,
+ canCherryPickInCurrentMR: true,
+ revertInForkPath: false,
+ canRevertInCurrentMR: true,
+ canRemoveSourceBranch: true,
+ sourceBranchRemoved: true,
+ metrics: {
+ mergedBy: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl:
+ 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ readableMergedAt: '',
+ closedBy: {},
+ closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ readableClosedAt: '',
+ },
+ updatedAt: 'mergedUpdatedAt',
+ shortMergeCommitSha: '958c0475',
+ mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed',
+ mergeCommitPath:
+ 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
+ sourceBranch: 'bar',
+ targetBranch,
+ };
+
+ const service = {
+ removeSourceBranch() {},
+ };
+
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ vm = mountComponent(Component, { mr, service });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('shouldShowRemoveSourceBranch', () => {
+ it('returns true when sourceBranchRemoved is false', () => {
+ vm.mr.sourceBranchRemoved = false;
+
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(true);
+ });
+
+ it('returns false when sourceBranchRemoved is true', () => {
+ vm.mr.sourceBranchRemoved = true;
+
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+
+ it('returns false when canRemoveSourceBranch is false', () => {
+ vm.mr.sourceBranchRemoved = false;
+ vm.mr.canRemoveSourceBranch = false;
+
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+
+ it('returns false when is making request', () => {
+ vm.mr.canRemoveSourceBranch = true;
+ vm.isMakingRequest = true;
+
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+
+ it('returns true when all are true', () => {
+ vm.mr.isRemovingSourceBranch = true;
+ vm.mr.canRemoveSourceBranch = true;
+ vm.isMakingRequest = true;
+
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+ });
+
+ describe('shouldShowSourceBranchRemoving', () => {
+ it('should correct value when fields changed', () => {
+ vm.mr.sourceBranchRemoved = false;
+
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(false);
+
+ vm.mr.sourceBranchRemoved = true;
+
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+
+ vm.mr.sourceBranchRemoved = false;
+ vm.isMakingRequest = true;
+
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
+
+ vm.isMakingRequest = false;
+ vm.mr.isRemovingSourceBranch = true;
+
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('removeSourceBranch', () => {
+ it('should set flag and call service then request main component to update the widget', done => {
+ jest.spyOn(vm.service, 'removeSourceBranch').mockReturnValue(
+ new Promise(resolve => {
+ resolve({
+ data: {
+ message: 'Branch was deleted',
+ },
+ });
+ }),
+ );
+
+ vm.removeSourceBranch();
+ setImmediate(() => {
+ const args = eventHub.$emit.mock.calls[0];
+
+ expect(vm.isMakingRequest).toEqual(true);
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).not.toThrow();
+ done();
+ });
+ });
+ });
+ });
+
+ it('has merged by information', () => {
+ expect(vm.$el.textContent).toContain('Merged by');
+ expect(vm.$el.textContent).toContain('Administrator');
+ });
+
+ it('renders branch information', () => {
+ expect(vm.$el.textContent).toContain('The changes were merged into');
+ expect(vm.$el.textContent).toContain(targetBranch);
+ });
+
+ it('renders information about branch being deleted', () => {
+ expect(vm.$el.textContent).toContain('The source branch has been deleted');
+ });
+
+ it('shows revert and cherry-pick buttons', () => {
+ expect(vm.$el.textContent).toContain('Revert');
+ expect(vm.$el.textContent).toContain('Cherry-pick');
+ });
+
+ it('shows button to copy commit SHA to clipboard', () => {
+ expect(selectors.copyMergeShaButton).toExist();
+ expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
+ vm.mr.mergeCommitSha,
+ );
+ });
+
+ it('hides button to copy commit SHA if SHA does not exist', done => {
+ vm.mr.mergeCommitSha = null;
+
+ Vue.nextTick(() => {
+ expect(selectors.copyMergeShaButton).not.toExist();
+ expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
+ done();
+ });
+ });
+
+ it('shows merge commit SHA link', () => {
+ expect(selectors.mergeCommitShaLink).toExist();
+ expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha);
+ expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
+ });
+
+ it('should not show source branch deleted text', done => {
+ vm.mr.sourceBranchRemoved = false;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('You can delete the source branch now');
+ expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
+ done();
+ });
+ });
+
+ it('should show source branch deleting text', done => {
+ vm.mr.isRemovingSourceBranch = true;
+ vm.mr.sourceBranchRemoved = false;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('The source branch is being deleted');
+ expect(vm.$el.innerText).not.toContain('You can delete the source branch now');
+ expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
+ done();
+ });
+ });
+
+ it('should use mergedEvent mergedAt as tooltip title', () => {
+ expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toBe(
+ 'Jan 24, 2018 1:02pm GMT+0000',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
new file mode 100644
index 00000000000..222cb74cc66
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
+
+describe('MRWidgetMerging', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(mergingComponent);
+
+ vm = mountComponent(Component, {
+ mr: {
+ targetBranchPath: '/branch-path',
+ targetBranch: 'branch',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders information about merge request being merged', () => {
+ expect(
+ vm.$el
+ .querySelector('.media-body')
+ .textContent.trim()
+ .replace(/\s\s+/g, ' ')
+ .replace(/[\r\n]+/g, ' '),
+ ).toContain('This merge request is in the process of being merged');
+ });
+
+ it('renders branch information', () => {
+ expect(
+ vm.$el
+ .querySelector('.mr-info-list')
+ .textContent.trim()
+ .replace(/\s\s+/g, ' ')
+ .replace(/[\r\n]+/g, ' '),
+ ).toEqual('The changes will be merged into branch');
+
+ expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('/branch-path');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
new file mode 100644
index 00000000000..3f03ebdb047
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
+
+describe('MRWidgetMissingBranch', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(missingBranchComponent);
+ vm = mountComponent(Component, { mr: { sourceBranchRemoved: true } });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('missingBranchName', () => {
+ it('should return proper branch name', () => {
+ expect(vm.missingBranchName).toEqual('source');
+
+ vm.mr.sourceBranchRemoved = false;
+
+ expect(vm.missingBranchName).toEqual('target');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = vm.$el;
+ const content = el.textContent.replace(/\n(\s)+/g, ' ').trim();
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(content.replace(/\s\s+/g, ' ')).toContain('source branch does not exist.');
+ expect(content).toContain('Please restore it or use a different source branch');
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
new file mode 100644
index 00000000000..63e93074857
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
+
+describe('MRWidgetNotAllowed', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(notAllowedComponent);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders success icon', () => {
+ expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null);
+ });
+
+ it('renders informative text', () => {
+ expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
+ expect(vm.$el.innerText).toContain(
+ 'Ask someone with write access to this repository to merge this request',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
new file mode 100644
index 00000000000..bd0bd36ebc2
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import NothingToMerge from '~/vue_merge_request_widget/components/states/nothing_to_merge.vue';
+
+describe('NothingToMerge', () => {
+ describe('template', () => {
+ const Component = Vue.extend(NothingToMerge);
+ const newBlobPath = '/foo';
+ const vm = new Component({
+ el: document.createElement('div'),
+ propsData: {
+ mr: { newBlobPath },
+ },
+ });
+
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('a').href).toContain(newBlobPath);
+ expect(vm.$el.innerText).toContain(
+ "Currently there are no changes in this merge request's source branch",
+ );
+
+ expect(vm.$el.innerText.replace(/\s\s+/g, ' ')).toContain(
+ 'Please push new commits or use a different branch.',
+ );
+ });
+
+ it('should not show new blob link if there is no link available', () => {
+ vm.mr.newBlobPath = null;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('a')).toEqual(null);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
new file mode 100644
index 00000000000..8847e4e6bdd
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { removeBreakLine } from 'helpers/text_helper';
+import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
+
+describe('MRWidgetPipelineBlocked', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(pipelineBlockedComponent);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders warning icon', () => {
+ expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null);
+ });
+
+ it('renders information text', () => {
+ expect(removeBreakLine(vm.$el.textContent).trim()).toContain(
+ 'Pipeline blocked. The pipeline for this merge request requires a manual action to proceed',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
new file mode 100644
index 00000000000..179adef12d9
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import { removeBreakLine } from 'helpers/text_helper';
+import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
+
+describe('PipelineFailed', () => {
+ describe('template', () => {
+ const Component = Vue.extend(PipelineFailed);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(removeBreakLine(vm.$el.innerText).trim()).toContain(
+ 'The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
new file mode 100644
index 00000000000..1f0d6a7378c
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -0,0 +1,981 @@
+import Vue from 'vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
+import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
+import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
+import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
+import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import { MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import simplePoll from '~/lib/utils/simple_poll';
+
+jest.mock('~/lib/utils/simple_poll', () =>
+ jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default),
+);
+jest.mock('~/commons/nav/user_merge_requests', () => ({
+ refreshUserMergeRequestCounts: jest.fn(),
+}));
+
+const commitMessage = 'This is the commit message';
+const squashCommitMessage = 'This is the squash commit message';
+const commitMessageWithDescription = 'This is the commit message description';
+const createTestMr = customConfig => {
+ const mr = {
+ isPipelineActive: false,
+ pipeline: null,
+ isPipelineFailed: false,
+ isPipelinePassing: false,
+ isMergeAllowed: true,
+ isApproved: true,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ ffOnlyEnabled: false,
+ hasCI: false,
+ ciStatus: null,
+ sha: '12345678',
+ squash: false,
+ commitMessage,
+ squashCommitMessage,
+ commitMessageWithDescription,
+ shouldRemoveSourceBranch: true,
+ canRemoveSourceBranch: false,
+ targetBranch: 'master',
+ preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
+ availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
+ mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
+ };
+
+ Object.assign(mr, customConfig.mr);
+
+ return mr;
+};
+
+const createTestService = () => ({
+ merge: jest.fn(),
+ poll: jest.fn().mockResolvedValue(),
+});
+
+const createComponent = (customConfig = {}) => {
+ const Component = Vue.extend(ReadyToMerge);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: {
+ mr: createTestMr(customConfig),
+ service: createTestService(),
+ },
+ });
+};
+
+describe('ReadyToMerge', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = ReadyToMerge.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
+ expect(vm.useCommitMessageWithDescription).toBeFalsy();
+ expect(vm.showCommitMessageEditor).toBeFalsy();
+ expect(vm.isMakingRequest).toBeFalsy();
+ expect(vm.isMergingImmediately).toBeFalsy();
+ expect(vm.commitMessage).toBe(vm.mr.commitMessage);
+ expect(vm.successSvg).toBeDefined();
+ expect(vm.warningSvg).toBeDefined();
+ });
+ });
+
+ describe('computed', () => {
+ describe('isAutoMergeAvailable', () => {
+ it('should return true when at least one merge strategy is available', () => {
+ vm.mr.availableAutoMergeStrategies = [MWPS_MERGE_STRATEGY];
+
+ expect(vm.isAutoMergeAvailable).toBe(true);
+ });
+
+ it('should return false when no merge strategies are available', () => {
+ vm.mr.availableAutoMergeStrategies = [];
+
+ expect(vm.isAutoMergeAvailable).toBe(false);
+ });
+ });
+
+ describe('status', () => {
+ it('defaults to success', () => {
+ Vue.set(vm.mr, 'pipeline', true);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns failed when MR has CI but also has an unknown status', () => {
+ Vue.set(vm.mr, 'hasCI', true);
+
+ expect(vm.status).toEqual('failed');
+ });
+
+ it('returns default when MR has no pipeline', () => {
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns pending when pipeline is active', () => {
+ Vue.set(vm.mr, 'pipeline', {});
+ Vue.set(vm.mr, 'isPipelineActive', true);
+
+ expect(vm.status).toEqual('pending');
+ });
+
+ it('returns failed when pipeline is failed', () => {
+ Vue.set(vm.mr, 'pipeline', {});
+ Vue.set(vm.mr, 'isPipelineFailed', true);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.status).toEqual('failed');
+ });
+ });
+
+ describe('mergeButtonVariant', () => {
+ it('defaults to success class', () => {
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.mergeButtonVariant).toEqual('success');
+ });
+
+ it('returns success class for success status', () => {
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+ Vue.set(vm.mr, 'pipeline', true);
+
+ expect(vm.mergeButtonVariant).toEqual('success');
+ });
+
+ it('returns info class for pending status', () => {
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', [MTWPS_MERGE_STRATEGY]);
+
+ expect(vm.mergeButtonVariant).toEqual('info');
+ });
+
+ it('returns danger class for failed status', () => {
+ vm.mr.hasCI = true;
+
+ expect(vm.mergeButtonVariant).toEqual('danger');
+ });
+ });
+
+ describe('status icon', () => {
+ it('defaults to tick icon', () => {
+ expect(vm.iconClass).toEqual('success');
+ });
+
+ it('shows tick for success status', () => {
+ vm.mr.pipeline = true;
+
+ expect(vm.iconClass).toEqual('success');
+ });
+
+ it('shows tick for pending status', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+
+ expect(vm.iconClass).toEqual('success');
+ });
+
+ it('shows warning icon for failed status', () => {
+ vm.mr.hasCI = true;
+
+ expect(vm.iconClass).toEqual('warning');
+ });
+
+ it('shows warning icon for merge not allowed', () => {
+ vm.mr.hasCI = true;
+
+ expect(vm.iconClass).toEqual('warning');
+ });
+ });
+
+ describe('mergeButtonText', () => {
+ it('should return "Merge" when no auto merge strategies are available', () => {
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.mergeButtonText).toEqual('Merge');
+ });
+
+ it('should return "Merge in progress"', () => {
+ Vue.set(vm, 'isMergingImmediately', true);
+
+ expect(vm.mergeButtonText).toEqual('Merge in progress');
+ });
+
+ it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
+ Vue.set(vm, 'isMergingImmediately', false);
+ Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY);
+
+ expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds');
+ });
+ });
+
+ describe('autoMergeText', () => {
+ it('should return Merge when pipeline succeeds', () => {
+ Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY);
+
+ expect(vm.autoMergeText).toEqual('Merge when pipeline succeeds');
+ });
+ });
+
+ describe('shouldShowMergeImmediatelyDropdown', () => {
+ it('should return false if no pipeline is active', () => {
+ Vue.set(vm.mr, 'isPipelineActive', false);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false);
+
+ expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false);
+ });
+
+ it('should return false if "Pipelines must succeed" is enabled for the current project', () => {
+ Vue.set(vm.mr, 'isPipelineActive', true);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true);
+
+ expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false);
+ });
+
+ it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => {
+ Vue.set(vm.mr, 'isPipelineActive', true);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false);
+
+ expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true);
+ });
+ });
+
+ describe('isMergeButtonDisabled', () => {
+ it('should return false with initial data', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', true);
+
+ expect(vm.isMergeButtonDisabled).toBe(false);
+ });
+
+ it('should return true when there is no commit message', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', true);
+ Vue.set(vm, 'commitMessage', '');
+
+ expect(vm.isMergeButtonDisabled).toBe(true);
+ });
+
+ it('should return true if merge is not allowed', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', false);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true);
+
+ expect(vm.isMergeButtonDisabled).toBe(true);
+ });
+
+ it('should return true when the vm instance is making request', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', true);
+ Vue.set(vm, 'isMakingRequest', true);
+
+ expect(vm.isMergeButtonDisabled).toBe(true);
+ });
+ });
+
+ describe('isMergeImmediatelyDangerous', () => {
+ it('should always return false in CE', () => {
+ expect(vm.isMergeImmediatelyDangerous).toBe(false);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('shouldShowMergeControls', () => {
+ it('should return false when an external pipeline is running and required to succeed', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', false);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.shouldShowMergeControls).toBe(false);
+ });
+
+ it('should return true when the build succeeded or build not required to succeed', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', true);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+
+ expect(vm.shouldShowMergeControls).toBe(true);
+ });
+
+ it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', false);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]);
+
+ expect(vm.shouldShowMergeControls).toBe(true);
+ });
+
+ it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
+ Vue.set(vm.mr, 'isMergeAllowed', true);
+ Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]);
+
+ expect(vm.shouldShowMergeControls).toBe(true);
+ });
+ });
+
+ describe('updateMergeCommitMessage', () => {
+ it('should revert flag and change commitMessage', () => {
+ expect(vm.commitMessage).toEqual(commitMessage);
+ vm.updateMergeCommitMessage(true);
+
+ expect(vm.commitMessage).toEqual(commitMessageWithDescription);
+ vm.updateMergeCommitMessage(false);
+
+ expect(vm.commitMessage).toEqual(commitMessage);
+ });
+ });
+
+ describe('handleMergeButtonClick', () => {
+ const returnPromise = status =>
+ new Promise(resolve => {
+ resolve({
+ data: {
+ status,
+ },
+ });
+ });
+
+ it('should handle merge when pipeline succeeds', done => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest
+ .spyOn(vm.service, 'merge')
+ .mockReturnValue(returnPromise('merge_when_pipeline_succeeds'));
+ vm.removeSourceBranch = false;
+ vm.handleMergeButtonClick(true);
+
+ setImmediate(() => {
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+
+ const params = vm.service.merge.mock.calls[0][0];
+
+ expect(params).toEqual(
+ expect.objectContaining({
+ sha: vm.mr.sha,
+ commit_message: vm.mr.commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
+ done();
+ });
+ });
+
+ it('should handle merge failed', done => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('failed'));
+ vm.handleMergeButtonClick(false, true);
+
+ setImmediate(() => {
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
+
+ const params = vm.service.merge.mock.calls[0][0];
+
+ expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.auto_merge_strategy).toBeUndefined();
+ done();
+ });
+ });
+
+ it('should handle merge action accepted case', done => {
+ jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('success'));
+ jest.spyOn(vm, 'initiateMergePolling').mockImplementation(() => {});
+ vm.handleMergeButtonClick();
+
+ setImmediate(() => {
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(vm.initiateMergePolling).toHaveBeenCalled();
+
+ const params = vm.service.merge.mock.calls[0][0];
+
+ expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.auto_merge_strategy).toBeUndefined();
+ done();
+ });
+ });
+ });
+
+ describe('initiateMergePolling', () => {
+ it('should call simplePoll', () => {
+ vm.initiateMergePolling();
+
+ expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 });
+ });
+
+ it('should call handleMergePolling', () => {
+ jest.spyOn(vm, 'handleMergePolling').mockImplementation(() => {});
+
+ vm.initiateMergePolling();
+
+ expect(vm.handleMergePolling).toHaveBeenCalled();
+ });
+ });
+
+ describe('handleMergePolling', () => {
+ const returnPromise = state =>
+ new Promise(resolve => {
+ resolve({
+ data: {
+ state,
+ source_branch_exists: true,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ loadFixtures('merge_requests/merge_request_of_current_user.html');
+ });
+
+ it('should call start and stop polling when MR merged', done => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
+ jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleMergePolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
+ setImmediate(() => {
+ expect(vm.service.poll).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent');
+ expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled();
+ expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+ expect(cpc).toBeFalsy();
+ expect(spc).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('updates status box', done => {
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
+ jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
+
+ vm.handleMergePolling(() => {}, () => {});
+
+ setImmediate(() => {
+ const statusBox = document.querySelector('.status-box');
+
+ expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy();
+ expect(statusBox.textContent).toContain('Merged');
+
+ done();
+ });
+ });
+
+ it('hides close button', done => {
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
+ jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
+
+ vm.handleMergePolling(() => {}, () => {});
+
+ setImmediate(() => {
+ expect(document.querySelector('.btn-close').classList.contains('hidden')).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('updates merge request count badge', done => {
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged'));
+ jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
+
+ vm.handleMergePolling(() => {}, () => {});
+
+ setImmediate(() => {
+ expect(document.querySelector('.js-merge-counter').textContent).toBe('0');
+
+ done();
+ });
+ });
+
+ it('should continue polling until MR is merged', done => {
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('some_other_state'));
+ jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {});
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleMergePolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
+ setImmediate(() => {
+ expect(cpc).toBeTruthy();
+ expect(spc).toBeFalsy();
+
+ done();
+ });
+ });
+ });
+
+ describe('initiateRemoveSourceBranchPolling', () => {
+ it('should emit event and call simplePoll', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ vm.initiateRemoveSourceBranchPolling();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
+ expect(simplePoll).toHaveBeenCalled();
+ });
+ });
+
+ describe('handleRemoveBranchPolling', () => {
+ const returnPromise = state =>
+ new Promise(resolve => {
+ resolve({
+ data: {
+ source_branch_exists: state,
+ },
+ });
+ });
+
+ it('should call start and stop polling when MR merged', done => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(false));
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleRemoveBranchPolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
+ setImmediate(() => {
+ expect(vm.service.poll).toHaveBeenCalled();
+
+ const args = eventHub.$emit.mock.calls[0];
+
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).toBeDefined();
+ args[1]();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
+
+ expect(cpc).toBeFalsy();
+ expect(spc).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('should continue polling until MR is merged', done => {
+ jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(true));
+
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
+
+ vm.handleRemoveBranchPolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
+ setImmediate(() => {
+ expect(cpc).toBeTruthy();
+ expect(spc).toBeFalsy();
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('Remove source branch checkbox', () => {
+ describe('when user can merge but cannot delete branch', () => {
+ it('should be disabled in the rendered output', () => {
+ const checkboxElement = vm.$el.querySelector('#remove-source-branch-input');
+
+ expect(checkboxElement).toBeNull();
+ });
+ });
+
+ describe('when user can merge and can delete branch', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { canRemoveSourceBranch: true },
+ });
+ });
+
+ it('isRemoveSourceBranchButtonDisabled should be false', () => {
+ expect(vm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ });
+
+ it('removed source branch should be enabled in rendered output', () => {
+ const checkboxElement = vm.$el.querySelector('#remove-source-branch-input');
+
+ expect(checkboxElement).not.toBeNull();
+ });
+ });
+ });
+
+ describe('render children components', () => {
+ let wrapper;
+ const localVue = createLocalVue();
+
+ const createLocalComponent = (customConfig = {}) => {
+ wrapper = shallowMount(localVue.extend(ReadyToMerge), {
+ localVue,
+ propsData: {
+ mr: createTestMr(customConfig),
+ service: createTestService(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
+ const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
+ const findCommitEditElements = () => wrapper.findAll(CommitEdit);
+ const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
+ const findFirstCommitEditLabel = () =>
+ findCommitEditElements()
+ .at(0)
+ .props('label');
+
+ describe('squash checkbox', () => {
+ it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: { commitsCount: 2, enableSquashBeforeMerge: true },
+ });
+
+ expect(findCheckboxElement().exists()).toBeTruthy();
+ });
+
+ it('should not be rendered when squash before merge is disabled', () => {
+ createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
+
+ expect(findCheckboxElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered when there is only 1 commit', () => {
+ createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } });
+
+ expect(findCheckboxElement().exists()).toBeFalsy();
+ });
+ });
+
+ describe('commits count collapsible header', () => {
+ it('should be rendered when fast-forward is disabled', () => {
+ createLocalComponent();
+
+ expect(findCommitsHeaderElement().exists()).toBeTruthy();
+ });
+
+ describe('when fast-forward is enabled', () => {
+ it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ enableSquashBeforeMerge: true,
+ squash: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeTruthy();
+ });
+
+ it('should not be rendered if squash before merge is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ enableSquashBeforeMerge: false,
+ squash: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: false,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered if commits count is 1', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 1,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+ });
+ });
+
+ describe('commits edit components', () => {
+ describe('when fast-forward merge is enabled', () => {
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: false,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should not be rendered if squash before merge is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: false,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should not be rendered if there is only one commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 1,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should have one edit component if squash is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(1);
+ expect(findFirstCommitEditLabel()).toBe('Squash commit message');
+ });
+ });
+
+ it('should have one edit component when squash is disabled', () => {
+ createLocalComponent();
+
+ expect(findCommitEditElements().length).toBe(1);
+ });
+
+ it('should have two edit components when squash is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ commitsCount: 2,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(2);
+ });
+
+ it('should have one edit components when squash is enabled and there is 1 commit only', () => {
+ createLocalComponent({
+ mr: {
+ commitsCount: 1,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(1);
+ });
+
+ it('should have correct edit merge commit label', () => {
+ createLocalComponent();
+
+ expect(findFirstCommitEditLabel()).toBe('Merge commit message');
+ });
+
+ it('should have correct edit squash commit label', () => {
+ createLocalComponent({
+ mr: {
+ commitsCount: 2,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ },
+ });
+
+ expect(findFirstCommitEditLabel()).toBe('Squash commit message');
+ });
+ });
+
+ describe('commits dropdown', () => {
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent();
+
+ expect(findCommitDropdownElement().exists()).toBeFalsy();
+ });
+
+ it('should be rendered if squash is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: { enableSquashBeforeMerge: true, squash: true, commitsCount: 2 },
+ });
+
+ expect(findCommitDropdownElement().exists()).toBeTruthy();
+ });
+ });
+ });
+
+ describe('Merge controls', () => {
+ describe('when allowed to merge', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { isMergeAllowed: true, canRemoveSourceBranch: true },
+ });
+ });
+
+ it('shows remove source branch checkbox', () => {
+ expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).not.toBeNull();
+ });
+
+ it('shows modify commit message button', () => {
+ expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
+ });
+
+ it('does not show message about needing to resolve items', () => {
+ expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull();
+ });
+ });
+
+ describe('when not allowed to merge', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { isMergeAllowed: false },
+ });
+ });
+
+ it('does not show remove source branch checkbox', () => {
+ expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull();
+ });
+
+ it('shows message to resolve all items before being allowed to merge', () => {
+ expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined();
+ });
+ });
+ });
+
+ describe('Merge request project settings', () => {
+ describe('when the merge commit merge method is enabled', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { ffOnlyEnabled: false },
+ });
+ });
+
+ it('should not show fast forward message', () => {
+ expect(vm.$el.querySelector('.mr-fast-forward-message')).toBeNull();
+ });
+
+ it('should show "Modify commit message" button', () => {
+ expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
+ });
+ });
+
+ describe('when the fast-forward merge method is enabled', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { ffOnlyEnabled: true },
+ });
+ });
+
+ it('should show fast forward message', () => {
+ expect(vm.$el.querySelector('.mr-fast-forward-message')).toBeDefined();
+ });
+
+ it('should not show "Modify commit message" button', () => {
+ expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
+ });
+ });
+ });
+
+ describe('with a mismatched SHA', () => {
+ const findMismatchShaBlock = () => vm.$el.querySelector('.js-sha-mismatch');
+
+ beforeEach(() => {
+ vm = createComponent({
+ mr: {
+ isSHAMismatch: true,
+ mergeRequestDiffsPath: '/merge_requests/1/diffs',
+ },
+ });
+ });
+
+ it('displays a warning message', () => {
+ expect(findMismatchShaBlock()).toExist();
+ });
+
+ it('warns the user to refresh to review', () => {
+ expect(findMismatchShaBlock().textContent.trim()).toBe(
+ 'New changes were added. Reload the page to review them',
+ );
+ });
+
+ it('displays link to the diffs tab', () => {
+ expect(findMismatchShaBlock().querySelector('a').href).toContain(vm.mr.mergeRequestDiffsPath);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
new file mode 100644
index 00000000000..38920846a50
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { removeBreakLine } from 'helpers/text_helper';
+import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
+
+describe('ShaMismatch', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ShaMismatch);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render information message', () => {
+ expect(vm.$el.querySelector('button').disabled).toEqual(true);
+
+ expect(removeBreakLine(vm.$el.textContent).trim()).toContain(
+ 'The source branch HEAD has recently changed. Please reload the page and review the changes before merging',
+ );
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
new file mode 100644
index 00000000000..b70d580ed04
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -0,0 +1,99 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
+
+const localVue = createLocalVue();
+
+describe('Squash before merge component', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ wrapper = shallowMount(localVue.extend(SquashBeforeMerge), {
+ localVue,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('checkbox', () => {
+ const findCheckbox = () => wrapper.find('.js-squash-checkbox');
+
+ it('is unchecked if passed value prop is false', () => {
+ createComponent({
+ value: false,
+ });
+
+ expect(findCheckbox().element.checked).toBeFalsy();
+ });
+
+ it('is checked if passed value prop is true', () => {
+ createComponent({
+ value: true,
+ });
+
+ expect(findCheckbox().element.checked).toBeTruthy();
+ });
+
+ it('changes value on click', done => {
+ createComponent({
+ value: false,
+ });
+
+ findCheckbox().element.checked = true;
+
+ findCheckbox().trigger('change');
+
+ wrapper.vm.$nextTick(() => {
+ expect(findCheckbox().element.checked).toBeTruthy();
+ done();
+ });
+ });
+
+ it('is disabled if isDisabled prop is true', () => {
+ createComponent({
+ value: false,
+ isDisabled: true,
+ });
+
+ expect(findCheckbox().attributes('disabled')).toBeTruthy();
+ });
+ });
+
+ describe('about link', () => {
+ it('is not rendered if no help path is passed', () => {
+ createComponent({
+ value: false,
+ });
+
+ const aboutLink = wrapper.find('a');
+
+ expect(aboutLink.exists()).toBeFalsy();
+ });
+
+ it('is rendered if help path is passed', () => {
+ createComponent({
+ value: false,
+ helpPath: 'test-path',
+ });
+
+ const aboutLink = wrapper.find('a');
+
+ expect(aboutLink.exists()).toBeTruthy();
+ });
+
+ it('should have a correct help path if passed', () => {
+ createComponent({
+ value: false,
+ helpPath: 'test-path',
+ });
+
+ const aboutLink = wrapper.find('a');
+
+ expect(aboutLink.attributes('href')).toEqual('test-path');
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
new file mode 100644
index 00000000000..33e52f4fd36
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('UnresolvedDiscussions', () => {
+ const Component = Vue.extend(UnresolvedDiscussions);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with threads path', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ createIssueToResolveDiscussionsPath: TEST_HOST,
+ },
+ });
+ });
+
+ it('should have correct elements', () => {
+ expect(vm.$el.innerText).toContain(
+ 'There are unresolved threads. Please resolve these threads',
+ );
+
+ expect(vm.$el.innerText).toContain('Create an issue to resolve them later');
+ expect(vm.$el.querySelector('.js-create-issue').getAttribute('href')).toEqual(TEST_HOST);
+ });
+ });
+
+ describe('without threads path', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {} });
+ });
+
+ it('should not show create issue link if user cannot create issue', () => {
+ expect(vm.$el.innerText).toContain(
+ 'There are unresolved threads. Please resolve these threads',
+ );
+
+ expect(vm.$el.querySelector('.js-create-issue')).toEqual(null);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
new file mode 100644
index 00000000000..6fa555b4fc4
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
+
+const createComponent = () => {
+ const Component = Vue.extend(WorkInProgress);
+ const mr = {
+ title: 'The best MR ever',
+ removeWIPPath: '/path/to/remove/wip',
+ };
+ const service = {
+ removeWIP() {},
+ };
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr, service },
+ });
+};
+
+describe('Wip', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr, service } = WorkInProgress.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+
+ expect(service.type instanceof Object).toBeTruthy();
+ expect(service.required).toBeTruthy();
+ });
+ });
+
+ describe('data', () => {
+ it('should have default data', () => {
+ const vm = createComponent();
+
+ expect(vm.isMakingRequest).toBeFalsy();
+ });
+ });
+
+ describe('methods', () => {
+ const mrObj = {
+ is_new_mr_data: true,
+ };
+
+ describe('handleRemoveWIP', () => {
+ it('should make a request to service and handle response', done => {
+ const vm = createComponent();
+
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(vm.service, 'removeWIP').mockReturnValue(
+ new Promise(resolve => {
+ resolve({
+ data: mrObj,
+ });
+ }),
+ );
+
+ vm.handleRemoveWIP();
+ setImmediate(() => {
+ expect(vm.isMakingRequest).toBeTruthy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'The merge request can now be merged.',
+ 'notice',
+ );
+ done();
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have correct elements', () => {
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('This is a Work in Progress');
+ expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(el.querySelector('button').innerText).toContain('Merge');
+ expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain(
+ 'Resolve WIP status',
+ );
+ });
+
+ it('should not show removeWIP button is user cannot update MR', done => {
+ vm.mr.removeWIPPath = '';
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-remove-wip')).toEqual(null);
+ done();
+ });
+ });
+ });
+});