diff options
Diffstat (limited to 'spec/frontend/vue_mr_widget/components')
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(); + }); + }); + }); +}); |