diff options
author | blackst0ne <blackst0ne.ru@gmail.com> | 2017-08-18 14:26:11 +1100 |
---|---|---|
committer | blackst0ne <blackst0ne.ru@gmail.com> | 2017-08-18 14:26:11 +1100 |
commit | 2047991363844b4f17216f94778b432ed198a412 (patch) | |
tree | 6b90710c44dae00178ad323d796beb59737f47f1 /spec/javascripts/vue_mr_widget/components | |
parent | 7a1c5ba7471b233c994f5b5b313085d0d1292b5d (diff) | |
parent | f3203cbbc25586df622552153cc460d2f79f414e (diff) | |
download | gitlab-ce-2047991363844b4f17216f94778b432ed198a412.tar.gz |
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'spec/javascripts/vue_mr_widget/components')
26 files changed, 2360 insertions, 0 deletions
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js new file mode 100644 index 00000000000..a750bc78f36 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author'; + +const author = { + webUrl: 'http://foo.bar', + avatarUrl: 'http://gravatar.com/foo', + name: 'fatihacet', +}; +const createComponent = () => { + const Component = Vue.extend(authorComponent); + + return new Component({ + el: document.createElement('div'), + propsData: { author }, + }); +}; + +describe('MRWidgetAuthor', () => { + describe('props', () => { + it('should have props', () => { + const authorProp = authorComponent.props.author; + + expect(authorProp).toBeDefined(); + expect(authorProp.type instanceof Object).toBeTruthy(); + expect(authorProp.required).toBeTruthy(); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const el = createComponent().$el; + + expect(el.tagName).toEqual('A'); + expect(el.getAttribute('href')).toEqual(author.webUrl); + expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl); + expect(el.querySelector('.author').innerText.trim()).toEqual(author.name); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js new file mode 100644 index 00000000000..515ddcbb875 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -0,0 +1,61 @@ +import Vue from 'vue'; +import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time'; + +const props = { + actionText: 'Merged by', + author: { + webUrl: 'http://foo.bar', + avatarUrl: 'http://gravatar.com/foo', + name: 'fatihacet', + }, + dateTitle: '2017-03-23T23:02:00.807Z', + dateReadable: '12 hours ago', +}; +const createComponent = () => { + const Component = Vue.extend(authorTimeComponent); + + return new Component({ + el: document.createElement('div'), + propsData: props, + }); +}; + +describe('MRWidgetAuthorTime', () => { + describe('props', () => { + it('should have props', () => { + const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props; + const ActionTextClass = actionText.type; + const DateTitleClass = dateTitle.type; + const DateReadableClass = dateReadable.type; + + expect(new ActionTextClass() instanceof String).toBeTruthy(); + expect(actionText.required).toBeTruthy(); + + expect(author.type instanceof Object).toBeTruthy(); + expect(author.required).toBeTruthy(); + + expect(new DateTitleClass() instanceof String).toBeTruthy(); + expect(dateTitle.required).toBeTruthy(); + + expect(new DateReadableClass() instanceof String).toBeTruthy(); + expect(dateReadable.required).toBeTruthy(); + }); + }); + + describe('components', () => { + it('should have components', () => { + expect(authorTimeComponent.components['mr-widget-author']).toBeDefined(); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const el = createComponent().$el; + + expect(el.tagName).toEqual('H4'); + expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl); + expect(el.querySelector('time').innerText).toContain(props.dateReadable); + expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js new file mode 100644 index 00000000000..7ee998c8fce --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -0,0 +1,179 @@ +import Vue from 'vue'; +import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment'; +import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; + +const deploymentMockData = [ + { + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', + external_url: 'http://diplo.', + external_url_formatted: 'diplo.', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + }, +]; +const createComponent = () => { + const Component = Vue.extend(deploymentComponent); + const mr = { + deployments: deploymentMockData, + }; + const service = {}; + + return new Component({ + el: document.createElement('div'), + propsData: { mr, service }, + }); +}; + +describe('MRWidgetDeployment', () => { + describe('props', () => { + it('should have props', () => { + const { mr, service } = deploymentComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + + expect(service.type instanceof Object).toBeTruthy(); + expect(service.required).toBeTruthy(); + }); + }); + + describe('methods', () => { + let vm = createComponent(); + const deployment = deploymentMockData[0]; + + describe('formatDate', () => { + it('should work', () => { + const readable = gl.utils.getTimeago().format(deployment.deployed_at); + expect(vm.formatDate(deployment.deployed_at)).toEqual(readable); + }); + }); + + describe('hasExternalUrls', () => { + it('should return true', () => { + expect(vm.hasExternalUrls(deployment)).toBeTruthy(); + }); + + it('should return false when there is not enough information', () => { + expect(vm.hasExternalUrls()).toBeFalsy(); + expect(vm.hasExternalUrls({ external_url: 'Diplo' })).toBeFalsy(); + expect(vm.hasExternalUrls({ external_url_formatted: 'Diplo' })).toBeFalsy(); + }); + }); + + describe('hasDeploymentTime', () => { + it('should return true', () => { + expect(vm.hasDeploymentTime(deployment)).toBeTruthy(); + }); + + it('should return false when there is not enough information', () => { + expect(vm.hasDeploymentTime()).toBeFalsy(); + expect(vm.hasDeploymentTime({ deployed_at: 'Diplo' })).toBeFalsy(); + expect(vm.hasDeploymentTime({ deployed_at_formatted: 'Diplo' })).toBeFalsy(); + }); + }); + + describe('hasDeploymentMeta', () => { + it('should return true', () => { + expect(vm.hasDeploymentMeta(deployment)).toBeTruthy(); + }); + + it('should return false when there is not enough information', () => { + expect(vm.hasDeploymentMeta()).toBeFalsy(); + expect(vm.hasDeploymentMeta({ url: 'Diplo' })).toBeFalsy(); + expect(vm.hasDeploymentMeta({ name: 'Diplo' })).toBeFalsy(); + }); + }); + + describe('stopEnvironment', () => { + const url = '/foo/bar'; + const returnPromise = () => new Promise((resolve) => { + resolve({ + json() { + return { + redirect_url: url, + }; + }, + }); + }); + const mockStopEnvironment = () => { + vm.stopEnvironment(deploymentMockData); + return vm; + }; + + it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => { + spyOn(window, 'confirm').and.returnValue(true); + spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true)); + spyOn(gl.utils, 'visitUrl').and.returnValue(true); + vm = mockStopEnvironment(); + + expect(window.confirm).toHaveBeenCalled(); + expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url); + setTimeout(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith(url); + done(); + }, 333); + }); + + it('should show a confirm dialog but should not work if the dialog is rejected', () => { + spyOn(window, 'confirm').and.returnValue(false); + spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false)); + vm = mockStopEnvironment(); + + expect(window.confirm).toHaveBeenCalled(); + expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + let vm; + let el; + const [deployment] = deploymentMockData; + + beforeEach(() => { + vm = createComponent(deploymentMockData); + el = vm.$el; + }); + + it('should render template elements correctly', () => { + expect(el.classList.contains('mr-widget-heading')).toBeTruthy(); + expect(el.querySelector('.js-icon-link')).toBeDefined(); + expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(deployment.url); + expect(el.querySelector('.js-deploy-meta').innerText).toContain(deployment.name); + expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url); + expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted); + expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at)); + expect(el.querySelector('.js-mr-memory-usage')).toBeDefined(); + expect(el.querySelector('button')).toBeDefined(); + }); + + it('should list multiple deployments', (done) => { + vm.mr.deployments.push(deployment); + vm.mr.deployments.push(deployment); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.ci-widget').length).toEqual(3); + expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(3); + done(); + }); + }); + + it('should not have some elements when there is not enough data', (done) => { + vm.mr.deployments = [{}]; + + Vue.nextTick(() => { + expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0); + expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0); + expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0); + expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(0); + expect(el.querySelectorAll('.button').length).toEqual(0); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js new file mode 100644 index 00000000000..06f89fabf42 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -0,0 +1,105 @@ +import Vue from 'vue'; +import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header'; + +const createComponent = (mr) => { + const Component = Vue.extend(headerComponent); + return new Component({ + el: document.createElement('div'), + propsData: { mr }, + }); +}; + +describe('MRWidgetHeader', () => { + describe('props', () => { + it('should have props', () => { + const { mr } = headerComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + }); + }); + + describe('computed', () => { + let vm; + beforeEach(() => { + vm = createComponent({ + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '/foo/bar/mr-widget-refactor', + targetBranch: 'master', + }); + }); + + it('shouldShowCommitsBehindText', () => { + expect(vm.shouldShowCommitsBehindText).toBeTruthy(); + + vm.mr.divergedCommitsCount = 0; + expect(vm.shouldShowCommitsBehindText).toBeFalsy(); + }); + + it('commitsText', () => { + expect(vm.commitsText).toEqual('commits'); + + vm.mr.divergedCommitsCount = 1; + expect(vm.commitsText).toEqual('commit'); + }); + }); + + describe('template', () => { + let vm; + let el; + const sourceBranchPath = '/foo/bar/mr-widget-refactor'; + const mr = { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + }; + + beforeEach(() => { + vm = createComponent(mr); + el = vm.$el; + }); + + it('should render template elements correctly', () => { + expect(el.classList.contains('mr-source-target')).toBeTruthy(); + const sourceBranchLink = el.querySelectorAll('.label-branch')[0]; + const targetBranchLink = el.querySelectorAll('.label-branch')[1]; + const commitsCount = el.querySelector('.diverged-commits-count'); + + expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); + expect(targetBranchLink.textContent).toContain(mr.targetBranch); + expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); + expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath); + expect(commitsCount.textContent).toContain('12 commits behind'); + expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath); + + expect(el.textContent).toContain('Check out branch'); + expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath); + expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath); + }); + + it('should not have right action links if the MR state is not open', (done) => { + vm.mr.isOpen = false; + Vue.nextTick(() => { + expect(el.textContent).not.toContain('Check out branch'); + expect(el.querySelectorAll('.dropdown li a').length).toEqual(0); + done(); + }); + }); + + it('should not render diverged commits count if the MR has no diverged commits', (done) => { + vm.mr.divergedCommitsCount = null; + Vue.nextTick(() => { + expect(el.textContent).not.toContain('commits behind'); + expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js new file mode 100644 index 00000000000..2ae3adc1f93 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -0,0 +1,233 @@ +import Vue from 'vue'; +import memoryUsageComponent from '~/vue_merge_request_widget/components/mr_widget_memory_usage'; +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(memoryUsageComponent); + + 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 unchanged from 0MB to 0MB', + loadFailed: 'Failed to load deployment statistics', + metricsUnavailable: 'Deployment statistics are not available currently', +}; + +describe('MemoryUsage', () => { + let vm; + let el; + + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + describe('props', () => { + it('should have props with defaults', () => { + const { metricsUrl } = memoryUsageComponent.props; + const MetricsUrlTypeClass = metricsUrl.type; + + Vue.nextTick(() => { + expect(new MetricsUrlTypeClass() instanceof String).toBeTruthy(); + expect(metricsUrl.required).toBeTruthy(); + }); + }); + }); + + describe('data', () => { + it('should have default data', () => { + const data = memoryUsageComponent.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('memoryChangeType', () => { + it('should return "increased" if memoryFrom value is less than memoryTo value', () => { + vm.memoryFrom = 4.28; + vm.memoryTo = 9.13; + + expect(vm.memoryChangeType).toEqual('increased'); + }); + + it('should return "decreased" if memoryFrom value is less than memoryTo value', () => { + vm.memoryFrom = 9.13; + vm.memoryTo = 4.28; + + expect(vm.memoryChangeType).toEqual('decreased'); + }); + + it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => { + vm.memoryFrom = 1; + vm.memoryTo = 1; + + expect(vm.memoryChangeType).toEqual('unchanged'); + }); + }); + }); + + 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', () => { + vm.computeGraphData(metrics, deployment_time); + const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm; + + expect(hasMetrics).toBeTruthy(); + expect(memoryMetrics.length > 0).toBeTruthy(); + expect(deploymentTime).toEqual(deployment_time); + expect(memoryFrom).toEqual('9.13'); + expect(memoryTo).toEqual('4.28'); + }); + }); + + describe('loadMetrics', () => { + const returnServicePromise = () => new Promise((resolve) => { + resolve({ + json() { + return metricsMockData; + }, + }); + }); + + it('should load metrics data using MRWidgetService', (done) => { + spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true)); + spyOn(vm, 'computeGraphData'); + + vm.loadMetrics(); + setTimeout(() => { + expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url); + expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time); + done(); + }, 333); + }); + }); + }); + + 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) => { + vm.loadingMetrics = false; + vm.hasMetrics = true; + vm.loadFailed = false; + + 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/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js new file mode 100644 index 00000000000..4da4fc82c26 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js @@ -0,0 +1,51 @@ +import Vue from 'vue'; +import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help'; + +const props = { + missingBranch: 'this-is-not-the-branch-you-are-looking-for', +}; +const text = `If the ${props.missingBranch} branch exists in your local repository`; + +const createComponent = () => { + const Component = Vue.extend(mergeHelpComponent); + return new Component({ + el: document.createElement('div'), + propsData: props, + }); +}; + +describe('MRWidgetMergeHelp', () => { + describe('props', () => { + it('should have props', () => { + const { missingBranch } = mergeHelpComponent.props; + const MissingBranchTypeClass = missingBranch.type; + + expect(new MissingBranchTypeClass() instanceof String).toBeTruthy(); + expect(missingBranch.required).toBeFalsy(); + expect(missingBranch.default).toEqual(''); + }); + }); + + describe('template', () => { + let vm; + let el; + + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + it('should have the correct elements', () => { + expect(el.classList.contains('mr-widget-help')).toBeTruthy(); + expect(el.textContent).toContain(text); + }); + + it('should not show missing branch name if missingBranch props is not provided', (done) => { + vm.missingBranch = null; + Vue.nextTick(() => { + expect(el.textContent).not.toContain(text); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js new file mode 100644 index 00000000000..c763487d12f --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -0,0 +1,129 @@ +import Vue from 'vue'; +import { statusIconEntityMap } from '~/vue_shared/ci_status_icons'; +import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline'; +import mockData from '../mock_data'; + +const createComponent = (mr) => { + const Component = Vue.extend(pipelineComponent); + return new Component({ + el: document.createElement('div'), + propsData: { mr }, + }); +}; + +describe('MRWidgetPipeline', () => { + describe('props', () => { + it('should have props', () => { + const { mr } = pipelineComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + }); + }); + + describe('components', () => { + it('should have components added', () => { + expect(pipelineComponent.components['pipeline-stage']).toBeDefined(); + expect(pipelineComponent.components.ciIcon).toBeDefined(); + }); + }); + + describe('computed', () => { + describe('svg', () => { + it('should have the proper SVG icon', () => { + const vm = createComponent({ pipeline: mockData.pipeline }); + + expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed); + }); + }); + + describe('hasCIError', () => { + it('should return false when there is no CI error', () => { + const vm = createComponent({ + pipeline: mockData.pipeline, + hasCI: true, + ciStatus: 'success', + }); + + expect(vm.hasCIError).toBeFalsy(); + }); + + it('should return true when there is a CI error', () => { + const vm = createComponent({ + pipeline: mockData.pipeline, + hasCI: true, + ciStatus: null, + }); + + expect(vm.hasCIError).toBeTruthy(); + }); + }); + }); + + describe('template', () => { + let vm; + let el; + const { pipeline } = mockData; + const mr = { + hasCI: true, + ciStatus: 'success', + pipelineDetailedStatus: pipeline.details.status, + pipeline, + }; + + beforeEach(() => { + vm = createComponent(mr); + el = vm.$el; + }); + + it('should render template elements correctly', () => { + expect(el.classList.contains('mr-widget-heading')).toBeTruthy(); + expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1); + expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`); + expect(el.innerText).toContain('passed'); + expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path); + expect(el.querySelectorAll('.stage-container').length).toEqual(2); + expect(el.querySelector('.js-ci-error')).toEqual(null); + expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path); + expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id); + expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`); + }); + + it('should list single stage', (done) => { + pipeline.details.stages.splice(0, 1); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.stage-container button').length).toEqual(1); + done(); + }); + }); + + it('should not have stages when there is no stage', (done) => { + vm.mr.pipeline.details.stages = []; + + Vue.nextTick(() => { + expect(el.querySelectorAll('.stage-container button').length).toEqual(0); + done(); + }); + }); + + it('should not have coverage text when pipeline has no coverage info', (done) => { + vm.mr.pipeline.coverage = null; + + Vue.nextTick(() => { + expect(el.querySelector('.js-mr-coverage')).toEqual(null); + done(); + }); + }); + + it('should show CI error when there is a CI error', (done) => { + vm.mr.ciStatus = null; + + Vue.nextTick(() => { + expect(el.querySelectorAll('.js-ci-error').length).toEqual(1); + expect(el.innerText).toContain('Could not connect to the CI server'); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js new file mode 100644 index 00000000000..f86fb6a0b4b --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links'; + +const createComponent = (data) => { + const Component = Vue.extend(relatedLinksComponent); + + return new Component({ + el: document.createElement('div'), + propsData: data, + }); +}; + +describe('MRWidgetRelatedLinks', () => { + describe('props', () => { + it('should have props', () => { + const { relatedLinks } = relatedLinksComponent.props; + + expect(relatedLinks).toBeDefined(); + expect(relatedLinks.type instanceof Object).toBeTruthy(); + expect(relatedLinks.required).toBeTruthy(); + }); + }); + + describe('computed', () => { + const data = { + relatedLinks: { + closing: '/foo', + mentioned: '/foo', + assignToMe: '/foo', + }, + }; + + describe('hasLinks', () => { + it('should return correct value when we have links reference', () => { + const vm = createComponent(data); + expect(vm.hasLinks).toBeTruthy(); + + vm.relatedLinks.closing = null; + expect(vm.hasLinks).toBeTruthy(); + + vm.relatedLinks.mentioned = null; + expect(vm.hasLinks).toBeTruthy(); + + vm.relatedLinks.assignToMe = null; + expect(vm.hasLinks).toBeFalsy(); + }); + }); + + describe('closesText', () => { + it('returns correct text for open merge request', () => { + data.state = 'open'; + const vm = createComponent(data); + expect(vm.closesText).toEqual('Closes'); + }); + + it('returns correct text for closed merge request', () => { + data.state = 'closed'; + const vm = createComponent(data); + expect(vm.closesText).toEqual('Did not close'); + }); + + it('returns correct tense for merged request', () => { + data.state = 'merged'; + const vm = createComponent(data); + expect(vm.closesText).toEqual('Closed'); + }); + }); + }); + + describe('template', () => { + it('should have only have closing issues text', () => { + const 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', () => { + const 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', () => { + const 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', () => { + const 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/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js new file mode 100644 index 00000000000..4869fb17d96 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived'; + +describe('MRWidgetArchived', () => { + describe('template', () => { + it('should have correct elements', () => { + const Component = Vue.extend(archivedComponent); + const el = new Component({ + el: document.createElement('div'), + }).$el; + + expect(el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); + expect(el.querySelector('button').disabled).toBeTruthy(); + expect(el.innerText).toContain('This project is archived, write access has been disabled'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js new file mode 100644 index 00000000000..6042d7384d5 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed'; + +const mergeError = 'This is the merge error'; + +describe('MRWidgetAutoMergeFailed', () => { + describe('props', () => { + it('should have props', () => { + const mrProp = autoMergeFailedComponent.props.mr; + + expect(mrProp.type instanceof Object).toBeTruthy(); + expect(mrProp.required).toBeTruthy(); + }); + }); + + describe('template', () => { + const Component = Vue.extend(autoMergeFailedComponent); + const vm = new Component({ + el: document.createElement('div'), + propsData: { + mr: { mergeError }, + }, + }); + + it('should have correct elements', () => { + expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeFalsy(); + expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically'); + expect(vm.$el.innerText).toContain(mergeError); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js new file mode 100644 index 00000000000..6b7aa935ad3 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking'; + +describe('MRWidgetChecking', () => { + describe('template', () => { + it('should have correct elements', () => { + const Component = Vue.extend(checkingComponent); + const el = new Component({ + el: document.createElement('div'), + }).$el; + + expect(el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); + expect(el.querySelector('button').disabled).toBeTruthy(); + expect(el.innerText).toContain('Checking ability to merge automatically'); + expect(el.querySelector('i')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js new file mode 100644 index 00000000000..47303d1e80f --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -0,0 +1,51 @@ +import Vue from 'vue'; +import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed'; + +const mr = { + targetBranch: 'good-branch', + targetBranchPath: '/good-branch', + closedBy: { + name: 'Fatih Acet', + username: 'fatihacet', + }, + updatedAt: '2017-03-23T20:08:08.845Z', + closedAt: '1 day ago', +}; + +const createComponent = () => { + const Component = Vue.extend(closedComponent); + + return new Component({ + el: document.createElement('div'), + propsData: { mr }, + }).$el; +}; + +describe('MRWidgetClosed', () => { + describe('props', () => { + it('should have props', () => { + const mrProp = closedComponent.props.mr; + + expect(mrProp.type instanceof Object).toBeTruthy(); + expect(mrProp.required).toBeTruthy(); + }); + }); + + describe('components', () => { + it('should have components added', () => { + expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined(); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const el = createComponent(); + + expect(el.querySelector('h4').textContent).toContain('Closed by'); + expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name); + expect(el.textContent).toContain('The changes were not merged into'); + expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); + expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js new file mode 100644 index 00000000000..3b7b7d93662 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -0,0 +1,70 @@ +import Vue from 'vue'; +import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; + +const path = '/conflicts'; +const createComponent = () => { + const Component = Vue.extend(conflictsComponent); + + return new Component({ + el: document.createElement('div'), + propsData: { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, + }, + }); +}; + +describe('MRWidgetConflicts', () => { + describe('props', () => { + it('should have props', () => { + const { mr } = conflictsComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const el = createComponent().$el; + const resolveButton = el.querySelector('.js-resolve-conflicts-button'); + const mergeButton = el.querySelector('.mr-widget-body .btn'); + const mergeLocallyButton = el.querySelector('.js-merge-locally-button'); + + expect(el.textContent).toContain('There are merge conflicts'); + expect(el.textContent).not.toContain('ask someone with write access'); + expect(el.querySelector('.btn-success').disabled).toBeTruthy(); + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); + + describe('when user does not have permission to merge', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + vm.mr.canMerge = false; + }); + + it('should show proper message', (done) => { + Vue.nextTick(() => { + expect(vm.$el.textContent).toContain('ask someone with write access'); + done(); + }); + }); + + it('should not have action buttons', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(1); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toEqual(null); + expect(vm.$el.querySelector('.js-merge-locally-button')).toEqual(null); + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js new file mode 100644 index 00000000000..cef365eec8a --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +const mr = { + mergeError: 'Merge error happened.', +}; +const createComponent = () => { + const Component = Vue.extend(failedToMergeComponent); + return new Component({ + el: document.createElement('div'), + propsData: { mr }, + }); +}; + +describe('MRWidgetFailedToMerge', () => { + describe('data', () => { + it('should have default data', () => { + const data = failedToMergeComponent.data(); + + expect(data.timer).toEqual(10); + expect(data.isRefreshing).toBeFalsy(); + }); + }); + + describe('computed', () => { + describe('timerText', () => { + it('should return correct timer text', () => { + const vm = createComponent(); + expect(vm.timerText).toEqual('10 seconds'); + + vm.timer = 1; + expect(vm.timerText).toEqual('a second'); + }); + }); + }); + + describe('created', () => { + it('should disable polling', () => { + spyOn(eventHub, '$emit'); + createComponent(); + + expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling'); + }); + }); + + describe('methods', () => { + describe('refresh', () => { + it('should emit event to request component refresh', () => { + spyOn(eventHub, '$emit'); + const vm = createComponent(); + + expect(vm.isRefreshing).toBeFalsy(); + + vm.refresh(); + expect(vm.isRefreshing).toBeTruthy(); + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling'); + }); + }); + + describe('updateTimer', () => { + it('should update timer and emit event when timer end', () => { + const vm = createComponent(); + spyOn(vm, 'refresh'); + + expect(vm.timer).toEqual(10); + + for (let i = 0; i < 10; i++) { // eslint-disable-line + expect(vm.timer).toEqual(10 - i); + vm.updateTimer(); + } + + expect(vm.refresh).toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + let vm; + let el; + + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + it('should have correct elements', (done) => { + expect(el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(el.innerText).toContain('Merge error happened.'); + expect(el.innerText).toContain('Refreshing in 10 seconds'); + expect(el.innerText).not.toContain('Merge failed.'); + expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); + expect(el.querySelector('button').innerText).toContain('Merge'); + expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now'); + expect(el.querySelector('.js-refresh-label')).toEqual(null); + expect(el.innerText).not.toContain('Refreshing now'); + setTimeout(() => { + expect(el.innerText).toContain('Refreshing in 9 seconds'); + done(); + }, 1010); + }); + + it('should just generic merge failed message if merge_error is not available', (done) => { + vm.mr.mergeError = null; + + Vue.nextTick(() => { + expect(el.innerText).toContain('Merge failed.'); + expect(el.innerText).not.toContain('Merge error happened.'); + done(); + }); + }); + + it('should show refresh label when refresh requested', () => { + vm.refresh(); + Vue.nextTick(() => { + expect(el.innerText).not.toContain('Merge failed. Refreshing'); + expect(el.innerText).toContain('Refreshing now'); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js new file mode 100644 index 00000000000..237035648cf --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging'; + +describe('MRWidgetMerging', () => { + describe('props', () => { + it('should have props', () => { + const { mr } = mergingComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const Component = Vue.extend(mergingComponent); + const mr = { + targetBranchPath: '/branch-path', + targetBranch: 'branch', + }; + const el = new Component({ + el: document.createElement('div'), + propsData: { mr }, + }).$el; + + expect(el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(el.innerText).toContain('This merge request is in the process of being merged'); + expect(el.innerText).toContain('changes will be merged into'); + expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath); + expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js new file mode 100644 index 00000000000..9a71d0b47d7 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js @@ -0,0 +1,213 @@ +import Vue from 'vue'; +import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +const targetBranchPath = '/foo/bar'; +const targetBranch = 'foo'; +const sha = '1EA2EZ34'; + +const createComponent = () => { + const Component = Vue.extend(mwpsComponent); + const mr = { + shouldRemoveSourceBranch: false, + canRemoveSourceBranch: true, + canCancelAutomaticMerge: true, + mergeUserId: 1, + currentUserId: 1, + setToMWPSBy: {}, + sha, + targetBranchPath, + targetBranch, + }; + + const service = { + cancelAutomaticMerge() {}, + mergeResource: { + save() {}, + }, + }; + + return new Component({ + el: document.createElement('div'), + propsData: { mr, service }, + }); +}; + +describe('MRWidgetMergeWhenPipelineSucceeds', () => { + describe('props', () => { + it('should have props', () => { + const { mr, service } = mwpsComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + + expect(service.type instanceof Object).toBeTruthy(); + expect(service.required).toBeTruthy(); + }); + }); + + describe('components', () => { + it('should have components added', () => { + expect(mwpsComponent.components['mr-widget-author']).toBeDefined(); + }); + }); + + describe('data', () => { + it('should have default data', () => { + const data = mwpsComponent.data(); + + expect(data.isCancellingAutoMerge).toBeFalsy(); + expect(data.isRemovingSourceBranch).toBeFalsy(); + }); + }); + + describe('computed', () => { + describe('canRemoveSourceBranch', () => { + it('should return true when user is able to remove source branch', () => { + const vm = createComponent(); + + expect(vm.canRemoveSourceBranch).toBeTruthy(); + }); + + it('should return false when user id is not the same with who set the MWPS', () => { + const vm = createComponent(); + + 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', () => { + const vm = createComponent(); + + vm.mr.shouldRemoveSourceBranch = true; + expect(vm.canRemoveSourceBranch).toBeFalsy(); + }); + + it('should return false if user is not able to remove the source branch', () => { + const vm = createComponent(); + + vm.mr.canRemoveSourceBranch = false; + expect(vm.canRemoveSourceBranch).toBeFalsy(); + }); + }); + }); + + describe('methods', () => { + describe('cancelAutomaticMerge', () => { + it('should set flag and call service then tell main component to update the widget with data', (done) => { + const vm = createComponent(); + const mrObj = { + is_new_mr_data: true, + }; + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => { + resolve({ + json() { + return mrObj; + }, + }); + })); + + vm.cancelAutomaticMerge(); + setTimeout(() => { + expect(vm.isCancellingAutoMerge).toBeTruthy(); + expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); + done(); + }, 333); + }); + }); + + describe('removeSourceBranch', () => { + it('should set flag and call service then request main component to update the widget', (done) => { + const vm = createComponent(); + spyOn(eventHub, '$emit'); + spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => { + resolve({ + json() { + return { + status: 'merge_when_pipeline_succeeds', + }; + }, + }); + })); + + vm.removeSourceBranch(); + setTimeout(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + expect(vm.service.mergeResource.save).toHaveBeenCalledWith({ + sha, + merge_when_pipeline_succeeds: true, + should_remove_source_branch: true, + }); + done(); + }, 333); + }); + }); + }); + + 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('to be merged automatically when the pipeline succeeds'); + expect(el.innerText).toContain('The changes will be merged into'); + expect(el.innerText).toContain(targetBranch); + expect(el.innerText).toContain('The source branch will not be removed'); + expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge'); + expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); + expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch'); + expect(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(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy(); + done(); + }); + }); + + it('should show source branch will be removed text when it source branch set to remove', (done) => { + vm.mr.shouldRemoveSourceBranch = true; + + Vue.nextTick(() => { + const normalizedText = el.innerText.replace(/\s+/g, ' '); + expect(normalizedText).toContain('The source branch will be removed'); + expect(normalizedText).not.toContain('The source branch will not be removed'); + done(); + }); + }); + + it('should not show remove source branch button when user not able to remove source branch', (done) => { + vm.mr.currentUserId = 4; + + Vue.nextTick(() => { + expect(el.querySelector('.js-remove-source-branch')).toEqual(null); + done(); + }); + }); + + it('should disable remove source branch button when the action is in progress', (done) => { + vm.isRemovingSourceBranch = true; + + Vue.nextTick(() => { + expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js new file mode 100644 index 00000000000..afaa750199a --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -0,0 +1,174 @@ +import Vue from 'vue'; +import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +const targetBranch = 'foo'; + +const createComponent = () => { + const Component = Vue.extend(mergedComponent); + const mr = { + isRemovingSourceBranch: false, + cherryPickInForkPath: false, + canCherryPickInCurrentMR: true, + revertInForkPath: false, + canRevertInCurrentMR: true, + canRemoveSourceBranch: true, + sourceBranchRemoved: true, + mergedBy: {}, + mergedAt: '', + updatedAt: '', + targetBranch, + }; + + const service = { + removeSourceBranch() {}, + }; + + return new Component({ + el: document.createElement('div'), + propsData: { mr, service }, + }); +}; + +describe('MRWidgetMerged', () => { + describe('props', () => { + it('should have props', () => { + const { mr, service } = mergedComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + + expect(service.type instanceof Object).toBeTruthy(); + expect(service.required).toBeTruthy(); + }); + }); + + describe('components', () => { + it('should have components added', () => { + expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined(); + }); + }); + + describe('data', () => { + it('should have default data', () => { + const data = mergedComponent.data(); + + expect(data.isMakingRequest).toBeFalsy(); + }); + }); + + describe('computed', () => { + describe('shouldShowRemoveSourceBranch', () => { + it('should correct value when fields changed', () => { + const vm = createComponent(); + vm.mr.sourceBranchRemoved = false; + expect(vm.shouldShowRemoveSourceBranch).toBeTruthy(); + + vm.mr.sourceBranchRemoved = true; + expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + + vm.mr.sourceBranchRemoved = false; + vm.mr.canRemoveSourceBranch = false; + expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + + vm.mr.canRemoveSourceBranch = true; + vm.isMakingRequest = true; + expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + + vm.mr.isRemovingSourceBranch = true; + vm.mr.canRemoveSourceBranch = true; + vm.isMakingRequest = true; + expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + }); + }); + describe('shouldShowSourceBranchRemoving', () => { + it('should correct value when fields changed', () => { + const vm = createComponent(); + vm.mr.sourceBranchRemoved = false; + expect(vm.shouldShowSourceBranchRemoving).toBeFalsy(); + + vm.mr.sourceBranchRemoved = true; + expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + + vm.mr.sourceBranchRemoved = false; + vm.isMakingRequest = true; + expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + + vm.isMakingRequest = false; + vm.mr.isRemovingSourceBranch = true; + expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + }); + }); + }); + + describe('methods', () => { + describe('removeSourceBranch', () => { + it('should set flag and call service then request main component to update the widget', (done) => { + const vm = createComponent(); + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => { + resolve({ + json() { + return { + message: 'Branch was removed', + }; + }, + }); + })); + + vm.removeSourceBranch(); + setTimeout(() => { + const args = eventHub.$emit.calls.argsFor(0); + expect(vm.isMakingRequest).toBeTruthy(); + expect(args[0]).toEqual('MRWidgetUpdateRequested'); + expect(args[1]).not.toThrow(); + done(); + }, 333); + }); + }); + }); + + 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.querySelector('.js-mr-widget-author')).toBeDefined(); + expect(el.innerText).toContain('The changes were merged into'); + expect(el.innerText).toContain(targetBranch); + expect(el.innerText).toContain('The source branch has been removed'); + expect(el.innerText).toContain('Revert'); + expect(el.innerText).toContain('Cherry-pick'); + expect(el.innerText).not.toContain('You can remove source branch now'); + expect(el.innerText).not.toContain('The source branch is being removed'); + }); + + it('should not show source branch removed text', (done) => { + vm.mr.sourceBranchRemoved = false; + + Vue.nextTick(() => { + expect(el.innerText).toContain('You can remove source branch now'); + expect(el.innerText).not.toContain('The source branch has been removed'); + done(); + }); + }); + + it('should show source branch removing text', (done) => { + vm.mr.isRemovingSourceBranch = true; + vm.mr.sourceBranchRemoved = false; + + Vue.nextTick(() => { + expect(el.innerText).toContain('The source branch is being removed'); + expect(el.innerText).not.toContain('You can remove source branch now'); + expect(el.innerText).not.toContain('The source branch has been removed'); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js new file mode 100644 index 00000000000..720effb5c1c --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch'; + +const createComponent = () => { + const Component = Vue.extend(missingBranchComponent); + const mr = { + sourceBranchRemoved: true, + }; + + return new Component({ + el: document.createElement('div'), + propsData: { mr }, + }); +}; + +describe('MRWidgetMissingBranch', () => { + describe('props', () => { + it('should have props', () => { + const mrProp = missingBranchComponent.props.mr; + + expect(mrProp.type instanceof Object).toBeTruthy(); + expect(mrProp.required).toBeTruthy(); + }); + }); + + describe('components', () => { + it('should have components added', () => { + expect(missingBranchComponent.components['mr-widget-merge-help']).toBeDefined(); + }); + }); + + describe('computed', () => { + describe('missingBranchName', () => { + it('should return proper branch name', () => { + const vm = createComponent(); + expect(vm.missingBranchName).toEqual('source'); + + vm.mr.sourceBranchRemoved = false; + expect(vm.missingBranchName).toEqual('target'); + }); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const el = createComponent().$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).toContain('source branch does not exist.'); + expect(content).toContain('Please restore it or use a different source branch'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js new file mode 100644 index 00000000000..33f20ab132d --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js @@ -0,0 +1,17 @@ +import Vue from 'vue'; +import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed'; + +describe('MRWidgetNotAllowed', () => { + describe('template', () => { + const Component = Vue.extend(notAllowedComponent); + 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(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/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js new file mode 100644 index 00000000000..a8a02fa6b66 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import nothingToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge'; + +describe('MRWidgetNothingToMerge', () => { + describe('template', () => { + const Component = Vue.extend(nothingToMergeComponent); + 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).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/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js new file mode 100644 index 00000000000..d0702f9f503 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked'; + +describe('MRWidgetPipelineBlocked', () => { + describe('template', () => { + const Component = Vue.extend(pipelineBlockedComponent); + 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(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js new file mode 100644 index 00000000000..78bac1c61a5 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import pipelineFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_failed'; + +describe('MRWidgetPipelineFailed', () => { + describe('template', () => { + const Component = Vue.extend(pipelineFailedComponent); + 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(vm.$el.innerText).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/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js new file mode 100644 index 00000000000..c607c9746a4 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -0,0 +1,422 @@ +import Vue from 'vue'; +import readyToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_ready_to_merge'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import * as simplePoll from '~/lib/utils/simple_poll'; + +const commitMessage = 'This is the commit message'; +const commitMessageWithDescription = 'This is the commit message description'; +const createComponent = (customConfig = {}) => { + const Component = Vue.extend(readyToMergeComponent); + const mr = { + isPipelineActive: false, + pipeline: null, + isPipelineFailed: false, + onlyAllowMergeIfPipelineSucceeds: false, + hasCI: false, + ciStatus: null, + sha: '12345678', + commitMessage, + commitMessageWithDescription, + shouldRemoveSourceBranch: true, + canRemoveSourceBranch: false, + }; + + Object.assign(mr, customConfig.mr); + + const service = { + merge() {}, + poll() {}, + }; + + return new Component({ + el: document.createElement('div'), + propsData: { mr, service }, + }); +}; + +describe('MRWidgetReadyToMerge', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + describe('props', () => { + it('should have props', () => { + const { mr, service } = readyToMergeComponent.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.setToMergeWhenPipelineSucceeds).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('commitMessageLinkTitle', () => { + const withDesc = 'Include description in commit message'; + const withoutDesc = "Don't include description in commit message"; + + it('should return message with description', () => { + expect(vm.commitMessageLinkTitle).toEqual(withDesc); + }); + + it('should return message without description', () => { + vm.useCommitMessageWithDescription = true; + expect(vm.commitMessageLinkTitle).toEqual(withoutDesc); + }); + }); + + describe('mergeButtonClass', () => { + const defaultClass = 'btn btn-small btn-success accept-merge-request'; + const failedClass = `${defaultClass} btn-danger`; + const inActionClass = `${defaultClass} btn-info`; + + it('should return default class', () => { + vm.mr.pipeline = true; + expect(vm.mergeButtonClass).toEqual(defaultClass); + }); + + it('should return failed class when MR has CI but also has an unknown status', () => { + vm.mr.hasCI = true; + expect(vm.mergeButtonClass).toEqual(failedClass); + }); + + it('should return default class when MR has no pipeline', () => { + expect(vm.mergeButtonClass).toEqual(defaultClass); + }); + + it('should return in action class when pipeline is active', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.mergeButtonClass).toEqual(inActionClass); + }); + + it('should return failed class when pipeline is failed', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineFailed = true; + expect(vm.mergeButtonClass).toEqual(failedClass); + }); + }); + + describe('mergeButtonText', () => { + it('should return Merge', () => { + expect(vm.mergeButtonText).toEqual('Merge'); + }); + + it('should return Merge in progress', () => { + vm.isMergingImmediately = true; + expect(vm.mergeButtonText).toEqual('Merge in progress'); + }); + + it('should return Merge when pipeline succeeds', () => { + vm.isMergingImmediately = false; + vm.mr.isPipelineActive = true; + expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds'); + }); + }); + + describe('shouldShowMergeOptionsDropdown', () => { + it('should return false with initial data', () => { + expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy(); + }); + + it('should return true when pipeline active', () => { + vm.mr.isPipelineActive = true; + expect(vm.shouldShowMergeOptionsDropdown).toBeTruthy(); + }); + + it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => { + vm.mr.isPipelineActive = true; + vm.mr.onlyAllowMergeIfPipelineSucceeds = true; + expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy(); + }); + }); + + describe('isMergeButtonDisabled', () => { + it('should return false with initial data', () => { + expect(vm.isMergeButtonDisabled).toBeFalsy(); + }); + + it('should return true when there is no commit message', () => { + vm.commitMessage = ''; + expect(vm.isMergeButtonDisabled).toBeTruthy(); + }); + + it('should return true if merge is not allowed', () => { + vm.mr.onlyAllowMergeIfPipelineSucceeds = true; + vm.mr.isPipelineFailed = true; + expect(vm.isMergeButtonDisabled).toBeTruthy(); + }); + + it('should return true when there vm instance is making request', () => { + vm.isMakingRequest = true; + expect(vm.isMergeButtonDisabled).toBeTruthy(); + }); + }); + + describe('Remove source branch checkbox', () => { + describe('when user can merge but cannot delete branch', () => { + it('isRemoveSourceBranchButtonDisabled should be true', () => { + expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); + }); + + it('should be disabled in the rendered output', () => { + const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); + }); + }); + + describe('when user can merge and can delete branch', () => { + beforeEach(() => { + this.customVm = createComponent({ + mr: { canRemoveSourceBranch: true }, + }); + }); + + it('isRemoveSourceBranchButtonDisabled should be false', () => { + expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + }); + + it('should be enabled in rendered output', () => { + const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBeNull(); + }); + }); + }); + }); + + describe('methods', () => { + describe('isMergeAllowed', () => { + it('should return false with initial data', () => { + expect(vm.isMergeAllowed()).toBeTruthy(); + }); + + it('should return false when MR is set only merge when pipeline succeeds', () => { + vm.mr.onlyAllowMergeIfPipelineSucceeds = true; + expect(vm.isMergeAllowed()).toBeTruthy(); + }); + + it('should return true true', () => { + vm.mr.onlyAllowMergeIfPipelineSucceeds = true; + vm.mr.isPipelineFailed = true; + expect(vm.isMergeAllowed()).toBeFalsy(); + }); + }); + + describe('updateCommitMessage', () => { + it('should revert flag and change commitMessage', () => { + expect(vm.useCommitMessageWithDescription).toBeFalsy(); + expect(vm.commitMessage).toEqual(commitMessage); + vm.updateCommitMessage(); + expect(vm.useCommitMessageWithDescription).toBeTruthy(); + expect(vm.commitMessage).toEqual(commitMessageWithDescription); + vm.updateCommitMessage(); + expect(vm.useCommitMessageWithDescription).toBeFalsy(); + expect(vm.commitMessage).toEqual(commitMessage); + }); + }); + + describe('toggleCommitMessageEditor', () => { + it('should toggle showCommitMessageEditor flag', () => { + expect(vm.showCommitMessageEditor).toBeFalsy(); + vm.toggleCommitMessageEditor(); + expect(vm.showCommitMessageEditor).toBeTruthy(); + }); + }); + + describe('handleMergeButtonClick', () => { + const returnPromise = status => new Promise((resolve) => { + resolve({ + json() { + return { status }; + }, + }); + }); + + it('should handle merge when pipeline succeeds', (done) => { + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'merge').and.returnValue(returnPromise('merge_when_pipeline_succeeds')); + vm.removeSourceBranch = false; + vm.handleMergeButtonClick(true); + + setTimeout(() => { + expect(vm.setToMergeWhenPipelineSucceeds).toBeTruthy(); + expect(vm.isMakingRequest).toBeTruthy(); + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + + const params = vm.service.merge.calls.argsFor(0)[0]; + expect(params.sha).toEqual(vm.mr.sha); + expect(params.commit_message).toEqual(vm.mr.commitMessage); + expect(params.should_remove_source_branch).toBeFalsy(); + expect(params.merge_when_pipeline_succeeds).toBeTruthy(); + done(); + }, 333); + }); + + it('should handle merge failed', (done) => { + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'merge').and.returnValue(returnPromise('failed')); + vm.handleMergeButtonClick(false, true); + + setTimeout(() => { + expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy(); + expect(vm.isMakingRequest).toBeTruthy(); + expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined); + + const params = vm.service.merge.calls.argsFor(0)[0]; + expect(params.should_remove_source_branch).toBeTruthy(); + expect(params.merge_when_pipeline_succeeds).toBeFalsy(); + done(); + }, 333); + }); + + it('should handle merge action accepted case', (done) => { + spyOn(vm.service, 'merge').and.returnValue(returnPromise('success')); + spyOn(vm, 'initiateMergePolling'); + vm.handleMergeButtonClick(); + + setTimeout(() => { + expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy(); + expect(vm.isMakingRequest).toBeTruthy(); + expect(vm.initiateMergePolling).toHaveBeenCalled(); + + const params = vm.service.merge.calls.argsFor(0)[0]; + expect(params.should_remove_source_branch).toBeTruthy(); + expect(params.merge_when_pipeline_succeeds).toBeFalsy(); + done(); + }, 333); + }); + }); + + describe('initiateMergePolling', () => { + it('should call simplePoll', () => { + spyOn(simplePoll, 'default'); + vm.initiateMergePolling(); + expect(simplePoll.default).toHaveBeenCalled(); + }); + }); + + describe('handleMergePolling', () => { + const returnPromise = state => new Promise((resolve) => { + resolve({ + json() { + return { state, source_branch_exists: true }; + }, + }); + }); + + it('should call start and stop polling when MR merged', (done) => { + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + let cpc = false; // continuePollingCalled + let spc = false; // stopPollingCalled + + vm.handleMergePolling(() => { cpc = true; }, () => { spc = true; }); + setTimeout(() => { + expect(vm.service.poll).toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent'); + expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled(); + expect(cpc).toBeFalsy(); + expect(spc).toBeTruthy(); + + done(); + }, 333); + }); + + it('should continue polling until MR is merged', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + let cpc = false; // continuePollingCalled + let spc = false; // stopPollingCalled + + vm.handleMergePolling(() => { cpc = true; }, () => { spc = true; }); + setTimeout(() => { + expect(cpc).toBeTruthy(); + expect(spc).toBeFalsy(); + + done(); + }, 333); + }); + }); + + describe('initiateRemoveSourceBranchPolling', () => { + it('should emit event and call simplePoll', () => { + spyOn(eventHub, '$emit'); + spyOn(simplePoll, 'default'); + + vm.initiateRemoveSourceBranchPolling(); + expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]); + expect(simplePoll.default).toHaveBeenCalled(); + }); + }); + + describe('handleRemoveBranchPolling', () => { + const returnPromise = state => new Promise((resolve) => { + resolve({ + json() { + return { source_branch_exists: state }; + }, + }); + }); + + it('should call start and stop polling when MR merged', (done) => { + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'poll').and.returnValue(returnPromise(false)); + + let cpc = false; // continuePollingCalled + let spc = false; // stopPollingCalled + + vm.handleRemoveBranchPolling(() => { cpc = true; }, () => { spc = true; }); + setTimeout(() => { + expect(vm.service.poll).toHaveBeenCalled(); + + const args = eventHub.$emit.calls.argsFor(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(); + }, 333); + }); + + it('should continue polling until MR is merged', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise(true)); + + let cpc = false; // continuePollingCalled + let spc = false; // stopPollingCalled + + vm.handleRemoveBranchPolling(() => { cpc = true; }, () => { spc = true; }); + setTimeout(() => { + expect(cpc).toBeTruthy(); + expect(spc).toBeFalsy(); + + done(); + }, 333); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js new file mode 100644 index 00000000000..4c67504b642 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch'; + +describe('MRWidgetSHAMismatch', () => { + describe('template', () => { + const Component = Vue.extend(shaMismatchComponent); + 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(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js new file mode 100644 index 00000000000..fe87f110354 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import unresolvedDiscussionsComponent from '~/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions'; + +describe('MRWidgetUnresolvedDiscussions', () => { + describe('props', () => { + it('should have props', () => { + const { mr } = unresolvedDiscussionsComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + }); + }); + + describe('template', () => { + let el; + let vm; + const path = 'foo/bar'; + + beforeEach(() => { + const Component = Vue.extend(unresolvedDiscussionsComponent); + const mr = { + createIssueToResolveDiscussionsPath: path, + }; + vm = new Component({ + el: document.createElement('div'), + propsData: { mr }, + }); + el = vm.$el; + }); + + it('should have correct elements', () => { + expect(el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(el.innerText).toContain('There are unresolved discussions. Please resolve these discussions'); + expect(el.innerText).toContain('Create an issue to resolve them later'); + expect(el.querySelector('.js-create-issue').getAttribute('href')).toEqual(path); + }); + + it('should not show create issue button if user cannot create issue', (done) => { + vm.mr.createIssueToResolveDiscussionsPath = ''; + + Vue.nextTick(() => { + expect(el.querySelector('.js-create-issue')).toEqual(null); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js new file mode 100644 index 00000000000..2cb3aaa6951 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -0,0 +1,96 @@ +import Vue from 'vue'; +import wipComponent from '~/vue_merge_request_widget/components/states/mr_widget_wip'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +const createComponent = () => { + const Component = Vue.extend(wipComponent); + 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('MRWidgetWIP', () => { + describe('props', () => { + it('should have props', () => { + const { mr, service } = wipComponent.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('removeWIP', () => { + it('should make a request to service and handle response', (done) => { + const vm = createComponent(); + + spyOn(window, 'Flash').and.returnValue(true); + spyOn(eventHub, '$emit'); + spyOn(vm.service, 'removeWIP').and.returnValue(new Promise((resolve) => { + resolve({ + json() { + return mrObj; + }, + }); + })); + + vm.removeWIP(); + setTimeout(() => { + expect(vm.isMakingRequest).toBeTruthy(); + expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); + expect(window.Flash).toHaveBeenCalledWith('The merge request can now be merged.', 'notice'); + done(); + }, 333); + }); + }); + }); + + 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).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(); + }); + }); + }); +}); |