diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-05-09 14:09:07 +0100 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-05-09 14:09:07 +0100 |
commit | 0c3abe3ef92fe4d982b780397e8ace37a51aca45 (patch) | |
tree | 4781ef63fa315d5a2a0edff6340d38e84db8847b /spec/javascripts | |
parent | d3b124e473e051d424070b93c1e800fab7c08656 (diff) | |
parent | 387b44103c168d9a1b82997101deb60c61b6aaf1 (diff) | |
download | gitlab-ce-31053-pipeline-ux.tar.gz |
Merge branch 'master' into 31053-pipeline-ux31053-pipeline-ux
* master:
Fallback to default pattern when note does not belong to project
Merge request widget redesign
Fixed focused test in notes spec
Fixed UP arrow key not editing last comment in discussion
Fix skipped manual actions issue in pipeline processing
Fix notify_only_default_branch check for Slack service
Diffstat (limited to 'spec/javascripts')
34 files changed, 2809 insertions, 244 deletions
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js index 82b00b4c1ec..10a60620f49 100644 --- a/spec/javascripts/commit/pipelines/mock_data.js +++ b/spec/javascripts/commit/pipelines/mock_data.js @@ -61,6 +61,7 @@ export default { tag: false, branch: true, }, + coverage: '42.21', commit: { id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4', short_id: 'fbd79f04', diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 47d904b865b..a746a776548 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -16,6 +16,16 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont sha: merge_request.diff_head_sha ) end + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end render_views @@ -39,6 +49,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merged_merge_request) end + it 'merge_requests/diff_comment.html.raw' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request) + end + private def render_merge_request(fixture_file_name, merge_request) diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js new file mode 100644 index 00000000000..e54acfa8e44 --- /dev/null +++ b/spec/javascripts/merge_request_notes_spec.js @@ -0,0 +1,61 @@ +/* global Notes */ + +import 'vendor/autosize'; +import '~/gl_form'; +import '~/lib/utils/text_utility'; +import '~/render_gfm'; +import '~/render_math'; +import '~/notes'; + +describe('Merge request notes', () => { + window.gon = window.gon || {}; + window.gl = window.gl || {}; + gl.utils = gl.utils || {}; + + const fixture = 'merge_requests/diff_comment.html.raw'; + preloadFixtures(fixture); + + beforeEach(() => { + loadFixtures(fixture); + gl.utils.disableButtonIfEmptyField = _.noop; + window.project_uploads_path = 'http://test.host/uploads'; + $('body').data('page', 'projects:merge_requests:show'); + window.gon.current_user_id = $('.note:last').data('author-id'); + + return new Notes('', []); + }); + + describe('up arrow', () => { + it('edits last comment when triggered in main form', () => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = 38; + + spyOnEvent('.note:last .js-note-edit', 'click'); + + $('.js-note-text').trigger(upArrowEvent); + + expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); + }); + + it('edits last comment in discussion when triggered in discussion form', (done) => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = 38; + + spyOnEvent('.note-discussion .js-note-edit', 'click'); + + $('.js-discussion-reply-button').click(); + + setTimeout(() => { + expect( + $('.note-discussion .js-note-text'), + ).toExist(); + + $('.note-discussion .js-note-text').trigger(upArrowEvent); + + expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js deleted file mode 100644 index 88dae8c3e06..00000000000 --- a/spec/javascripts/merge_request_widget_spec.js +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ - -require('~/merge_request_widget'); -require('~/smart_interval'); -require('~/lib/utils/datetime_utility'); - -(function() { - describe('MergeRequestWidget', function() { - beforeEach(function() { - window.notifyPermissions = function() {}; - window.notify = function() {}; - this.opts = { - ci_status_url: "http://sampledomain.local/ci/getstatus", - ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus", - ci_status: "", - ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" - }, - ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" - }, - gitlab_icon: "gitlab_logo.png", - ci_pipeline: 80, - ci_sha: "12a34bc5", - builds_path: "http://sampledomain.local/sampleBuildsPath", - commits_path: "http://sampledomain.local/commits", - pipeline_path: "http://sampledomain.local/pipelines" - }; - this["class"] = new window.gl.MergeRequestWidget(this.opts); - }); - - describe('getCIEnvironmentsStatus', function() { - beforeEach(function() { - this.ciEnvironmentsStatusData = [{ - created_at: '2016-09-12T13:38:30.636Z', - environment_id: 1, - environment_name: 'env1', - external_url: 'https://test-url.com', - external_url_formatted: 'test-url.com' - }]; - - spyOn(jQuery, 'getJSON').and.callFake(function(req, cb) { - cb(this.ciEnvironmentsStatusData); - }.bind(this)); - }); - - it('should call renderEnvironments when the environments property is set', function() { - const spy = spyOn(this.class, 'renderEnvironments').and.stub(); - this.class.getCIEnvironmentsStatus(); - expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData); - }); - - it('should not call renderEnvironments when the environments property is not set', function() { - this.ciEnvironmentsStatusData = null; - const spy = spyOn(this.class, 'renderEnvironments').and.stub(); - this.class.getCIEnvironmentsStatus(); - expect(spy).not.toHaveBeenCalled(); - }); - }); - - describe('renderEnvironments', function() { - describe('should render correct timeago', function() { - beforeEach(function() { - this.environments = [{ - id: 'test-environment-id', - url: 'testurl', - deployed_at: new Date().toISOString(), - deployed_at_formatted: true - }]; - }); - - function getTimeagoText(template) { - var el = document.createElement('html'); - el.innerHTML = template; - return el.querySelector('.js-environment-timeago').innerText.trim(); - } - - it('should render less than a minute ago text', function() { - spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) { - expect(getTimeagoText(template)).toBe('less than a minute ago.'); - }); - - this.class.renderEnvironments(this.environments); - }); - - it('should render about an hour ago text', function() { - var oneHourAgo = new Date(); - oneHourAgo.setHours(oneHourAgo.getHours() - 1); - - this.environments[0].deployed_at = oneHourAgo.toISOString(); - spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) { - expect(getTimeagoText(template)).toBe('about an hour ago.'); - }); - - this.class.renderEnvironments(this.environments); - }); - - it('should render about 2 hours ago text', function() { - var twoHoursAgo = new Date(); - twoHoursAgo.setHours(twoHoursAgo.getHours() - 2); - - this.environments[0].deployed_at = twoHoursAgo.toISOString(); - spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) { - expect(getTimeagoText(template)).toBe('about 2 hours ago.'); - }); - - this.class.renderEnvironments(this.environments); - }); - }); - }); - - describe('mergeInProgress', function() { - it('should display error with h4 tag', function() { - spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) { - expect(html).toBe('<h4>Sorry, something went wrong.</h4>'); - }); - spyOn($, 'ajax').and.callFake(function(e) { - e.success({ merge_error: 'Sorry, something went wrong.' }); - }); - this.class.mergeInProgress(null); - }); - }); - - describe('getCIStatus', function() { - beforeEach(function() { - this.ciStatusData = { - "title": "Sample MR title", - "pipeline": 80, - "sha": "12a34bc5", - "status": "success", - "coverage": 98 - }; - - spyOn(jQuery, 'getJSON').and.callFake((function(_this) { - return function(req, cb) { - return cb(_this.ciStatusData); - }; - })(this)); - }); - it('should call showCIStatus even if a notification should not be displayed', function() { - var spy; - spy = spyOn(this["class"], 'showCIStatus').and.stub(); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"].getCIStatus(false); - return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); - }); - it('should call showCIStatus when a notification should be displayed', function() { - var spy; - spy = spyOn(this["class"], 'showCIStatus').and.stub(); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"].getCIStatus(true); - return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); - }); - it('should call showCICoverage when the coverage rate is set', function() { - var spy; - spy = spyOn(this["class"], 'showCICoverage').and.stub(); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"].getCIStatus(false); - return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); - }); - it('should not call showCICoverage when the coverage rate is not set', function() { - var spy; - this.ciStatusData.coverage = null; - spy = spyOn(this["class"], 'showCICoverage').and.stub(); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"].getCIStatus(false); - return expect(spy).not.toHaveBeenCalled(); - }); - it('should not display a notification on the first check after the widget has been created', function() { - var spy; - spy = spyOn(window, 'notify'); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"] = new window.gl.MergeRequestWidget(this.opts); - this["class"].getCIStatus(true); - return expect(spy).not.toHaveBeenCalled(); - }); - it('should update the pipeline URL when the pipeline changes', function() { - var spy; - spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"].getCIStatus(false); - this.ciStatusData.pipeline += 1; - this["class"].getCIStatus(false); - return expect(spy).toHaveBeenCalled(); - }); - it('should update the commit URL when the sha changes', function() { - var spy; - spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); - spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); - this["class"].getCIStatus(false); - this.ciStatusData.sha = "9b50b99a"; - this["class"].getCIStatus(false); - return expect(spy).toHaveBeenCalled(); - }); - }); - }); -}).call(window); diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js deleted file mode 100644 index b5c5e60dd97..00000000000 --- a/spec/javascripts/merged_buttons_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -/* global MergedButtons */ - -import '~/merged_buttons'; - -describe('MergedButtons', () => { - const fixturesPath = 'merge_requests/merged_merge_request.html.raw'; - preloadFixtures(fixturesPath); - - beforeEach(() => { - loadFixtures(fixturesPath); - this.mergedButtons = new MergedButtons(); - this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)'); - this.$removeBranchProgress = $('.remove_source_branch_in_progress'); - this.$removeBranchFailed = $('.remove_source_branch_widget.failed'); - this.$removeBranchButton = $('.remove_source_branch'); - }); - - describe('removeSourceBranch', () => { - it('shows loader', () => { - $('.remove_source_branch').trigger('click'); - expect(this.$removeBranchProgress).toBeVisible(); - expect(this.$removeBranchWidget).not.toBeVisible(); - }); - }); - - describe('removeBranchSuccess', () => { - it('refreshes page when branch removed', () => { - spyOn(gl.utils, 'refreshCurrentPage').and.stub(); - const response = { status: 200 }; - this.$removeBranchButton.trigger('ajax:success', response, 'xhr'); - expect(gl.utils.refreshCurrentPage).toHaveBeenCalled(); - }); - }); - - describe('removeBranchError', () => { - it('shows error message', () => { - const response = { status: 500 }; - this.$removeBranchButton.trigger('ajax:error', response, 'xhr'); - expect(this.$removeBranchFailed).toBeVisible(); - expect(this.$removeBranchProgress).not.toBeVisible(); - expect(this.$removeBranchWidget).not.toBeVisible(); - }); - }); -}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 92428fd501c..0464b5d2329 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -55,7 +55,6 @@ if (process.env.BABEL_ENV === 'coverage') { './merge_conflicts/merge_conflicts_bundle.js', './merge_conflicts/components/inline_conflict_lines.js', './merge_conflicts/components/parallel_conflict_lines.js', - './merge_request_widget/ci_bundle.js', './monitoring/monitoring_bundle.js', './network/network_bundle.js', './network/branch_graph.js', 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..3d5f71babfb --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -0,0 +1,184 @@ +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'; +import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons'; + +const deploymentMockData = [ + { + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + 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('computed', () => { + describe('svg', () => { + it('should have the proper SVG icon', () => { + const vm = createComponent(deploymentMockData); + expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success); + }); + }); + }); + + 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('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); + 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('.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..48f816c8460 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -0,0 +1,95 @@ +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 mr = { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '/foo/bar/mr-widget-refactor', + 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(); + expect(el.querySelectorAll('.label-branch')[0].textContent).toContain(mr.sourceBranch); + expect(el.querySelectorAll('.label-branch')[1].textContent).toContain(mr.targetBranch); + expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind'); + + 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_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..1b418c7dfcf --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -0,0 +1,131 @@ +import Vue from 'vue'; +import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_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['pipeline-status-icon']).toBeDefined(); + }); + }); + + describe('computed', () => { + describe('svg', () => { + it('should have the proper SVG icon', () => { + const vm = createComponent({ pipeline: mockData.pipeline }); + + expect(vm.svg).toEqual(statusClassToSvgMap.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.innerText).toContain('with stages'); + 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); + expect(el.innerText).toContain('with stage'); + 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..f6e0c3dfb74 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -0,0 +1,138 @@ +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', () => { + describe('hasLinks', () => { + it('should return correct value when we have links reference', () => { + const data = { + relatedLinks: { + closing: '/foo', + mentioned: '/foo', + assignToMe: '/foo', + }, + }; + 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('methods', () => { + const data = { + relatedLinks: { + closing: '<a href="#">#23</a> and <a>#42</a>', + mentioned: '<a href="#">#7</a>', + }, + }; + const vm = createComponent(data); + + describe('hasMultipleIssues', () => { + it('should return true if the given text has multiple issues', () => { + expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy(); + }); + + it('should return false if the given text has one issue', () => { + expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy(); + }); + }); + + describe('issueLabel', () => { + it('should return true if the given text has multiple issues', () => { + expect(vm.issueLabel('closing')).toEqual('issues'); + }); + + it('should return false if the given text has one issue', () => { + expect(vm.issueLabel('mentioned')).toEqual('issue'); + }); + }); + + describe('verbLabel', () => { + it('should return true if the given text has multiple issues', () => { + expect(vm.verbLabel('closing')).toEqual('are'); + }); + + it('should return false if the given text has one issue', () => { + expect(vm.verbLabel('mentioned')).toEqual('is'); + }); + }); + }); + + 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 issues #23 and #42'); + expect(content).not.toContain('mentioned'); + }); + + it('should have only have mentioned issues text', () => { + const vm = createComponent({ + relatedLinks: { + mentioned: '<a href="#">#7</a>', + }, + }); + + expect(vm.$el.innerText).toContain('issue #7'); + expect(vm.$el.innerText).toContain('is mentioned but will not be closed.'); + 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 issue #7.'); + expect(content).toContain('issues #23 and #42'); + expect(content).toContain('are mentioned but will not be closed.'); + }); + + 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..cac2f561a0b --- /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..47b4ba893e0 --- /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')).toBeTruthy(); + 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..3be11d47227 --- /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..78a70725e94 --- /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', + targetBranchCommitsPath: '/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.targetBranchCommitsPath); + 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..e7ae85caec4 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -0,0 +1,69 @@ +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.querySelectorAll('.btn-group .btn')[0]; + const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1]; + + 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(el.querySelectorAll('.btn-group .btn').length).toBe(2); + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + 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('a.js-resolve-conflicts-button')).toEqual(null); + expect(vm.$el.querySelector('a.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..587b83430d9 --- /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..fb2ef606604 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import lockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_locked'; + +describe('MRWidgetLocked', () => { + describe('props', () => { + it('should have props', () => { + const { mr } = lockedComponent.props; + + expect(mr.type instanceof Object).toBeTruthy(); + expect(mr.required).toBeTruthy(); + }); + }); + + describe('template', () => { + it('should have correct elements', () => { + const Component = Vue.extend(lockedComponent); + 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('it is locked'); + 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..8d8b90cea16 --- /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..6628010112d --- /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..98674d12afb --- /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 the source branch 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..61e00f4cf79 --- /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..d40c67b189d --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js @@ -0,0 +1,17 @@ +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 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('There is nothing to merge from source branch into target branch.'); + expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.'); + }); + }); +}); 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..b293d118571 --- /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..807fba705d4 --- /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..74df99415c9 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -0,0 +1,389 @@ +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 = () => { + const Component = Vue.extend(readyToMergeComponent); + const mr = { + isPipelineActive: false, + pipeline: null, + isPipelineFailed: false, + onlyAllowMergeIfPipelineSucceeds: false, + hasCI: false, + ciStatus: null, + sha: '12345678', + commitMessage, + commitMessageWithDescription, + }; + + 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.removeSourceBranch).toBeTruthy(true); + 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 wit 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-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('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_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..45bd1a69964 --- /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 merge request is currently Work In Progress and therefore unable to merge'); + 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(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js new file mode 100644 index 00000000000..e6f96d5588b --- /dev/null +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -0,0 +1,214 @@ +/* eslint-disable */ + +export default { + "id": 132, + "iid": 22, + "assignee_id": null, + "author_id": 1, + "description": "", + "lock_version": null, + "milestone_id": null, + "position": 0, + "state": "merged", + "title": "Update README.md", + "updated_by_id": null, + "created_at": "2017-04-07T12:27:26.718Z", + "updated_at": "2017-04-07T15:39:25.852Z", + "deleted_at": null, + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null, + "in_progress_merge_commit_sha": null, + "locked_at": null, + "merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775", + "merge_error": null, + "merge_params": { + "force_remove_source_branch": null + }, + "merge_status": "can_be_merged", + "merge_user_id": null, + "merge_when_pipeline_succeeds": false, + "source_branch": "daaaa", + "source_project_id": 19, + "target_branch": "master", + "target_project_id": 19, + "merge_event": { + "author": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "updated_at": "2017-04-07T15:39:25.696Z" + }, + "closed_event": null, + "author": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "merge_user": null, + "diff_head_sha": "104096c51715e12e7ae41f9333e9fa35b73f385d", + "diff_head_commit_short_id": "104096c5", + "merge_commit_message": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", + "pipeline": { + "id": 172, + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "active": false, + "coverage": "92.16", + "path": "/root/acets-app/pipelines/172", + "details": { + "status": { + "icon": "icon_status_success", + "favicon": "favicon_status_success", + "text": "passed", + "label": "passed", + "group": "success", + "has_details": true, + "details_path": "/root/acets-app/pipelines/172" + }, + "duration": null, + "finished_at": "2017-04-07T14:00:14.256Z", + "stages": [ + { + "name": "build", + "title": "build: failed", + "status": { + "icon": "icon_status_failed", + "favicon": "favicon_status_failed", + "text": "failed", + "label": "failed", + "group": "failed", + "has_details": true, + "details_path": "/root/acets-app/pipelines/172#build" + }, + "path": "/root/acets-app/pipelines/172#build", + "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=build" + }, + { + "name": "review", + "title": "review: skipped", + "status": { + "icon": "icon_status_skipped", + "favicon": "favicon_status_skipped", + "text": "skipped", + "label": "skipped", + "group": "skipped", + "has_details": true, + "details_path": "/root/acets-app/pipelines/172#review" + }, + "path": "/root/acets-app/pipelines/172#review", + "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=review" + } + ], + "artifacts": [ + + ], + "manual_actions": [ + { + "name": "stop_review", + "path": "/root/acets-app/builds/1427/play", + "playable": false + } + ] + }, + "flags": { + "latest": false, + "triggered": false, + "stuck": false, + "yaml_errors": false, + "retryable": true, + "cancelable": false + }, + "ref": { + "name": "daaaa", + "path": "/root/acets-app/tree/daaaa", + "tag": false, + "branch": true + }, + "commit": { + "id": "104096c51715e12e7ae41f9333e9fa35b73f385d", + "short_id": "104096c5", + "title": "Update README.md", + "created_at": "2017-04-07T15:27:18.000+03:00", + "parent_ids": [ + "2396536178668d8930c29d904e53bd4d06228b32" + ], + "message": "Update README.md", + "author_name": "Administrator", + "author_email": "admin@example.com", + "authored_date": "2017-04-07T15:27:18.000+03:00", + "committer_name": "Administrator", + "committer_email": "admin@example.com", + "committed_date": "2017-04-07T15:27:18.000+03:00", + "author": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + }, + "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d", + "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d" + }, + "retry_path": "/root/acets-app/pipelines/172/retry", + "created_at": "2017-04-07T12:27:19.520Z", + "updated_at": "2017-04-07T15:28:44.800Z" + }, + "work_in_progress": false, + "source_branch_exists": false, + "mergeable_discussions_state": true, + "conflicts_can_be_resolved_in_ui": false, + "branch_missing": true, + "commits_count": 1, + "has_conflicts": false, + "can_be_merged": true, + "has_ci": true, + "ci_status": "success", + "pipeline_status_path": "/root/acets-app/merge_requests/22/pipeline_status", + "issues_links": { + "closing": "", + "mentioned_but_not_closing": "" + }, + "current_user": { + "can_resolve_conflicts": true, + "can_remove_source_branch": false, + "can_revert_on_current_merge_request": true, + "can_cherry_pick_on_current_merge_request": true + }, + "target_branch_path": "/root/acets-app/branches/master", + "source_branch_path": "/root/acets-app/branches/daaaa", + "conflict_resolution_ui_path": "/root/acets-app/merge_requests/22/conflicts", + "remove_wip_path": "/root/acets-app/merge_requests/22/remove_wip", + "cancel_merge_when_pipeline_succeeds_path": "/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds", + "create_issue_to_resolve_discussions_path": "/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22", + "merge_path": "/root/acets-app/merge_requests/22/merge", + "cherry_pick_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", + "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", + "email_patches_path": "/root/acets-app/merge_requests/22.patch", + "plain_diff_path": "/root/acets-app/merge_requests/22.diff", + "ci_status_path": "/root/acets-app/merge_requests/22/ci_status", + "status_path": "/root/acets-app/merge_requests/22.json", + "merge_check_path": "/root/acets-app/merge_requests/22/merge_check", + "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status", + "project_archived": false, + "merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", + "diverged_commits_count": 0, + "only_allow_merge_if_pipeline_succeeds": false, + "commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content" +} diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js new file mode 100644 index 00000000000..22ee7dcf0e7 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -0,0 +1,326 @@ +import Vue from 'vue'; +import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; +import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import mockData from './mock_data'; + +const createComponent = () => { + delete mrWidgetOptions.el; // Prevent component mounting + gl.mrWidgetData = mockData; + const Component = Vue.extend(mrWidgetOptions); + return new Component(); +}; + +const returnPromise = data => new Promise((resolve) => { + resolve({ + json() { + return data; + }, + body: data, + }); +}); + +describe('mrWidgetOptions', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + describe('data', () => { + it('should instantiate Store and Service', () => { + expect(vm.mr).toBeDefined(); + expect(vm.service).toBeDefined(); + }); + }); + + describe('computed', () => { + describe('componentName', () => { + it('should return merged component', () => { + expect(vm.componentName).toEqual('mr-widget-merged'); + }); + + it('should return conflicts component', () => { + vm.mr.state = 'conflicts'; + expect(vm.componentName).toEqual('mr-widget-conflicts'); + }); + }); + + describe('shouldRenderMergeHelp', () => { + it('should return false for the initial merged state', () => { + expect(vm.shouldRenderMergeHelp).toBeFalsy(); + }); + + it('should return true for a state which requires help widget', () => { + vm.mr.state = 'conflicts'; + expect(vm.shouldRenderMergeHelp).toBeTruthy(); + }); + }); + + describe('shouldRenderPipelines', () => { + it('should return true for the initial data', () => { + expect(vm.shouldRenderPipelines).toBeTruthy(); + }); + + it('should return true when pipeline is empty but MR.hasCI is set to true', () => { + vm.mr.pipeline = {}; + expect(vm.shouldRenderPipelines).toBeTruthy(); + }); + + it('should return true when pipeline available', () => { + vm.mr.hasCI = false; + expect(vm.shouldRenderPipelines).toBeTruthy(); + }); + + it('should return false when there is no pipeline', () => { + vm.mr.pipeline = {}; + vm.mr.hasCI = false; + expect(vm.shouldRenderPipelines).toBeFalsy(); + }); + }); + + describe('shouldRenderRelatedLinks', () => { + it('should return false for the initial data', () => { + expect(vm.shouldRenderRelatedLinks).toBeFalsy(); + }); + + it('should return true if there is relatedLinks in MR', () => { + vm.mr.relatedLinks = {}; + expect(vm.shouldRenderRelatedLinks).toBeTruthy(); + }); + }); + + describe('shouldRenderDeployments', () => { + it('should return false for the initial data', () => { + expect(vm.shouldRenderDeployments).toBeFalsy(); + }); + + it('should return true if there is deployments', () => { + vm.mr.deployments.push({}, {}); + expect(vm.shouldRenderDeployments).toBeTruthy(); + }); + }); + }); + + describe('methods', () => { + describe('checkStatus', () => { + it('should tell service to check status', (done) => { + spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData)); + spyOn(vm.mr, 'setData'); + let isCbExecuted = false; + const cb = () => { + isCbExecuted = true; + }; + + vm.checkStatus(cb); + + setTimeout(() => { + expect(vm.service.checkStatus).toHaveBeenCalled(); + expect(vm.mr.setData).toHaveBeenCalled(); + expect(isCbExecuted).toBeTruthy(); + done(); + }, 333); + }); + }); + + describe('initPolling', () => { + it('should call SmartInterval', () => { + spyOn(gl, 'SmartInterval').and.returnValue({ + resume() {}, + stopTimer() {}, + }); + vm.initPolling(); + + expect(vm.pollingInterval).toBeDefined(); + expect(gl.SmartInterval).toHaveBeenCalled(); + }); + }); + + describe('initDeploymentsPolling', () => { + it('should call SmartInterval', () => { + spyOn(gl, 'SmartInterval'); + vm.initDeploymentsPolling(); + + expect(vm.deploymentsInterval).toBeDefined(); + expect(gl.SmartInterval).toHaveBeenCalled(); + }); + }); + + describe('fetchDeployments', () => { + it('should fetch deployments', (done) => { + spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ deployment: 1 }])); + + vm.fetchDeployments(); + + setTimeout(() => { + expect(vm.service.fetchDeployments).toHaveBeenCalled(); + expect(vm.mr.deployments.length).toEqual(1); + expect(vm.mr.deployments[0].deployment).toEqual(1); + done(); + }, 333); + }); + }); + + describe('fetchActionsContent', () => { + it('should fetch content of Cherry Pick and Revert modals', (done) => { + spyOn(vm.service, 'fetchMergeActionsContent').and.returnValue(returnPromise('hello world')); + + vm.fetchActionsContent(); + + setTimeout(() => { + expect(vm.service.fetchMergeActionsContent).toHaveBeenCalled(); + expect(document.body.textContent).toContain('hello world'); + done(); + }, 333); + }); + }); + + describe('bindEventHubListeners', () => { + it('should bind eventHub listeners', () => { + spyOn(vm, 'checkStatus').and.returnValue(() => {}); + spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData)); + spyOn(vm, 'fetchActionsContent'); + spyOn(vm.mr, 'setData'); + spyOn(vm, 'resumePolling'); + spyOn(vm, 'stopPolling'); + spyOn(eventHub, '$on'); + + vm.bindEventHubListeners(); + + eventHub.$emit('SetBranchRemoveFlag', ['flag']); + expect(vm.mr.isRemovingSourceBranch).toEqual('flag'); + + eventHub.$emit('FailedToMerge'); + expect(vm.mr.state).toEqual('failedToMerge'); + + eventHub.$emit('UpdateWidgetData', mockData); + expect(vm.mr.setData).toHaveBeenCalledWith(mockData); + + eventHub.$emit('EnablePolling'); + expect(vm.resumePolling).toHaveBeenCalled(); + + eventHub.$emit('DisablePolling'); + expect(vm.stopPolling).toHaveBeenCalled(); + + const listenersWithServiceRequest = { + MRWidgetUpdateRequested: true, + FetchActionsContent: true, + }; + + const allArgs = eventHub.$on.calls.allArgs(); + allArgs.forEach((params) => { + const eventName = params[0]; + const callback = params[1]; + + if (listenersWithServiceRequest[eventName]) { + listenersWithServiceRequest[eventName] = callback; + } + }); + + listenersWithServiceRequest.MRWidgetUpdateRequested(); + expect(vm.checkStatus).toHaveBeenCalled(); + + listenersWithServiceRequest.FetchActionsContent(); + expect(vm.fetchActionsContent).toHaveBeenCalled(); + }); + }); + + describe('handleMounted', () => { + it('should call required methods to do the initial kick-off', () => { + spyOn(vm, 'checkStatus'); + spyOn(vm, 'initDeploymentsPolling'); + spyOn(vm, 'setFavicon'); + + vm.handleMounted(); + + expect(vm.checkStatus).toHaveBeenCalled(); + expect(vm.setFavicon).toHaveBeenCalled(); + expect(vm.initDeploymentsPolling).toHaveBeenCalled(); + }); + }); + + describe('setFavicon', () => { + it('should call setFavicon method', () => { + spyOn(gl.utils, 'setFavicon'); + vm.setFavicon(); + + expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath); + }); + + it('should not call setFavicon when there is no ciStatusFaviconPath', () => { + spyOn(gl.utils, 'setFavicon'); + vm.mr.ciStatusFaviconPath = null; + vm.setFavicon(); + + expect(gl.utils.setFavicon).not.toHaveBeenCalled(); + }); + }); + + describe('resumePolling', () => { + it('should call stopTimer on pollingInterval', () => { + spyOn(vm.pollingInterval, 'resume'); + + vm.resumePolling(); + expect(vm.pollingInterval.resume).toHaveBeenCalled(); + }); + }); + + describe('stopPolling', () => { + it('should call stopTimer on pollingInterval', () => { + spyOn(vm.pollingInterval, 'stopTimer'); + + vm.stopPolling(); + expect(vm.pollingInterval.stopTimer).toHaveBeenCalled(); + }); + }); + + describe('createService', () => { + it('should instantiate a Service', () => { + const endpoints = { + mergePath: '/nice/path', + mergeCheckPath: '/nice/path', + cancelAutoMergePath: '/nice/path', + removeWIPPath: '/nice/path', + sourceBranchPath: '/nice/path', + ciEnvironmentsStatusPath: '/nice/path', + statusPath: '/nice/path', + mergeActionsContentPath: '/nice/path', + }; + + const serviceInstance = vm.createService(endpoints); + const isInstanceOfMRService = serviceInstance instanceof MRWidgetService; + expect(isInstanceOfMRService).toBe(true); + Object.keys(serviceInstance).forEach((key) => { + expect(serviceInstance[key]).toBeDefined(); + }); + }); + }); + }); + + describe('components', () => { + it('should register all components', () => { + const comps = mrWidgetOptions.components; + expect(comps['mr-widget-header']).toBeDefined(); + expect(comps['mr-widget-merge-help']).toBeDefined(); + expect(comps['mr-widget-pipeline']).toBeDefined(); + expect(comps['mr-widget-deployment']).toBeDefined(); + expect(comps['mr-widget-related-links']).toBeDefined(); + expect(comps['mr-widget-merged']).toBeDefined(); + expect(comps['mr-widget-closed']).toBeDefined(); + expect(comps['mr-widget-locked']).toBeDefined(); + expect(comps['mr-widget-failed-to-merge']).toBeDefined(); + expect(comps['mr-widget-wip']).toBeDefined(); + expect(comps['mr-widget-archived']).toBeDefined(); + expect(comps['mr-widget-conflicts']).toBeDefined(); + expect(comps['mr-widget-nothing-to-merge']).toBeDefined(); + expect(comps['mr-widget-not-allowed']).toBeDefined(); + expect(comps['mr-widget-missing-branch']).toBeDefined(); + expect(comps['mr-widget-ready-to-merge']).toBeDefined(); + expect(comps['mr-widget-checking']).toBeDefined(); + expect(comps['mr-widget-unresolved-discussions']).toBeDefined(); + expect(comps['mr-widget-pipeline-blocked']).toBeDefined(); + expect(comps['mr-widget-pipeline-failed']).toBeDefined(); + expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js new file mode 100644 index 00000000000..b63633c03b8 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; + +Vue.use(VueResource); + +describe('MRWidgetService', () => { + const mr = { + mergePath: './', + mergeCheckPath: './', + cancelAutoMergePath: './', + removeWIPPath: './', + sourceBranchPath: './', + ciEnvironmentsStatusPath: './', + statusPath: './', + mergeActionsContentPath: './', + isServiceStore: true, + }; + + it('should have store and resources created in constructor', () => { + const service = new MRWidgetService(mr); + + expect(service.mergeResource).toBeDefined(); + expect(service.mergeCheckResource).toBeDefined(); + expect(service.cancelAutoMergeResource).toBeDefined(); + expect(service.removeWIPResource).toBeDefined(); + expect(service.removeSourceBranchResource).toBeDefined(); + expect(service.deploymentsResource).toBeDefined(); + expect(service.pollResource).toBeDefined(); + expect(service.mergeActionsContentResource).toBeDefined(); + }); + + it('should have methods defined', () => { + const service = new MRWidgetService(mr); + + expect(service.merge()).toBeDefined(); + expect(service.cancelAutomaticMerge()).toBeDefined(); + expect(service.removeWIP()).toBeDefined(); + expect(service.removeSourceBranch()).toBeDefined(); + expect(service.fetchDeployments()).toBeDefined(); + expect(service.poll()).toBeDefined(); + expect(service.checkStatus()).toBeDefined(); + expect(service.fetchMergeActionsContent()).toBeDefined(); + expect(MRWidgetService.stopEnvironment()).toBeDefined(); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js new file mode 100644 index 00000000000..ee944f4d4e5 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js @@ -0,0 +1,62 @@ +import getStateKey from '~/vue_merge_request_widget/stores/get_state_key'; + +describe('getStateKey', () => { + it('should return proper state name', () => { + const context = { + mergeStatus: 'checked', + mergeWhenPipelineSucceeds: false, + canMerge: true, + onlyAllowMergeIfPipelineSucceeds: false, + isPipelineFailed: false, + hasMergeableDiscussionsState: false, + isPipelineBlocked: false, + canBeMerged: false, + }; + const data = { + project_archived: false, + branch_missing: false, + commits_count: 2, + has_conflicts: false, + work_in_progress: false, + }; + const bound = getStateKey.bind(context, data); + expect(bound()).toEqual(null); + + context.canBeMerged = true; + expect(bound()).toEqual('readyToMerge'); + + context.isPipelineBlocked = true; + expect(bound()).toEqual('pipelineBlocked'); + + context.hasMergeableDiscussionsState = true; + expect(bound()).toEqual('unresolvedDiscussions'); + + context.onlyAllowMergeIfPipelineSucceeds = true; + context.isPipelineFailed = true; + expect(bound()).toEqual('pipelineFailed'); + + context.canMerge = false; + expect(bound()).toEqual('notAllowedToMerge'); + + context.mergeWhenPipelineSucceeds = true; + expect(bound()).toEqual('mergeWhenPipelineSucceeds'); + + data.work_in_progress = true; + expect(bound()).toEqual('workInProgress'); + + data.has_conflicts = true; + expect(bound()).toEqual('conflicts'); + + context.mergeStatus = 'unchecked'; + expect(bound()).toEqual('checking'); + + data.commits_count = 0; + expect(bound()).toEqual('nothingToMerge'); + + data.branch_missing = true; + expect(bound()).toEqual('missingBranch'); + + data.project_archived = true; + expect(bound()).toEqual('archived'); + }); +}); |