diff options
Diffstat (limited to 'spec/javascripts')
24 files changed, 545 insertions, 231 deletions
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js new file mode 100644 index 00000000000..acd0aaf2a86 --- /dev/null +++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js @@ -0,0 +1,51 @@ +/* eslint-disable import/no-unresolved */ + +import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer'; +import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr'; + +describe('Balsamiq integration spec', () => { + let container; + let endpoint; + let balsamiqViewer; + + preloadFixtures('static/balsamiq_viewer.html.raw'); + + beforeEach(() => { + loadFixtures('static/balsamiq_viewer.html.raw'); + + container = document.getElementById('js-balsamiq-viewer'); + balsamiqViewer = new BalsamiqViewer(container); + }); + + describe('successful response', () => { + beforeEach((done) => { + endpoint = bmprPath; + + balsamiqViewer.loadFile(endpoint).then(done).catch(done.fail); + }); + + it('does not show loading icon', () => { + expect(document.querySelector('.loading')).toBeNull(); + }); + + it('renders the balsamiq previews', () => { + expect(document.querySelectorAll('.previews .preview').length).not.toEqual(0); + }); + }); + + describe('error getting file', () => { + beforeEach((done) => { + endpoint = 'invalid/path/to/file.bmpr'; + + balsamiqViewer.loadFile(endpoint).then(done.fail, null).catch(done); + }); + + it('does not show loading icon', () => { + expect(document.querySelector('.loading')).toBeNull(); + }); + + it('does not render the balsamiq previews', () => { + expect(document.querySelectorAll('.previews .preview').length).toEqual(0); + }); + }); +}); diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js index 85816ee1f11..aa87956109f 100644 --- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js +++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js @@ -4,17 +4,11 @@ import ClassSpecHelper from '../../helpers/class_spec_helper'; describe('BalsamiqViewer', () => { let balsamiqViewer; - let endpoint; let viewer; describe('class constructor', () => { beforeEach(() => { - endpoint = 'endpoint'; - viewer = { - dataset: { - endpoint, - }, - }; + viewer = {}; balsamiqViewer = new BalsamiqViewer(viewer); }); @@ -22,25 +16,25 @@ describe('BalsamiqViewer', () => { it('should set .viewer', () => { expect(balsamiqViewer.viewer).toBe(viewer); }); + }); + + describe('fileLoaded', () => { - it('should set .endpoint', () => { - expect(balsamiqViewer.endpoint).toBe(endpoint); - }); }); describe('loadFile', () => { let xhr; + let loadFile; + const endpoint = 'endpoint'; beforeEach(() => { - endpoint = 'endpoint'; xhr = jasmine.createSpyObj('xhr', ['open', 'send']); balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderFile']); - balsamiqViewer.endpoint = endpoint; spyOn(window, 'XMLHttpRequest').and.returnValue(xhr); - BalsamiqViewer.prototype.loadFile.call(balsamiqViewer); + loadFile = BalsamiqViewer.prototype.loadFile.call(balsamiqViewer, endpoint); }); it('should call .open', () => { @@ -54,6 +48,10 @@ describe('BalsamiqViewer', () => { it('should call .send', () => { expect(xhr.send).toHaveBeenCalled(); }); + + it('should return a promise', () => { + expect(loadFile).toEqual(jasmine.any(Promise)); + }); }); describe('renderFile', () => { @@ -325,18 +323,4 @@ describe('BalsamiqViewer', () => { expect(parseTitle).toBe('name'); }); }); - - describe('onError', () => { - beforeEach(() => { - spyOn(window, 'Flash'); - - BalsamiqViewer.onError(); - }); - - ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'onError'); - - it('should instantiate Flash', () => { - expect(window.Flash).toHaveBeenCalledWith('Balsamiq file could not be loaded.'); - }); - }); }); diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js index e7786e8cc2c..2bbcebeeac0 100644 --- a/spec/javascripts/droplab/drop_down_spec.js +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - import DropDown from '~/droplab/drop_down'; import utils from '~/droplab/utils'; import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants'; @@ -17,7 +15,7 @@ describe('DropDown', function () { it('sets the .hidden property to true', function () { expect(this.dropdown.hidden).toBe(true); - }) + }); it('sets the .list property', function () { expect(this.dropdown.list).toBe(this.list); @@ -152,7 +150,7 @@ describe('DropDown', function () { it('should call addSelectedClass', function () { expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement); - }) + }); it('should call .preventDefault', function () { expect(this.event.preventDefault).toHaveBeenCalled(); @@ -293,36 +291,6 @@ describe('DropDown', function () { }); }); - describe('toggle', function () { - beforeEach(function () { - this.dropdown = { hidden: true, show: () => {}, hide: () => {} }; - - spyOn(this.dropdown, 'show'); - spyOn(this.dropdown, 'hide'); - - DropDown.prototype.toggle.call(this.dropdown); - }); - - it('should call .show if hidden is true', function () { - expect(this.dropdown.show).toHaveBeenCalled(); - }); - - describe('if hidden is false', function () { - beforeEach(function () { - this.dropdown = { hidden: false, show: () => {}, hide: () => {} }; - - spyOn(this.dropdown, 'show'); - spyOn(this.dropdown, 'hide'); - - DropDown.prototype.toggle.call(this.dropdown); - }); - - it('should call .show if hidden is true', function () { - expect(this.dropdown.hide).toHaveBeenCalled(); - }); - }); - }); - describe('setData', function () { beforeEach(function () { this.dropdown = { render: () => {} }; @@ -399,7 +367,7 @@ describe('DropDown', function () { expect(this.data.map).toHaveBeenCalledWith(jasmine.any(Function)); }); - it('should call .renderChildren for each data item', function() { + it('should call .renderChildren for each data item', function () { expect(this.dropdown.renderChildren.calls.count()).toBe(this.data.length); }); @@ -407,7 +375,7 @@ describe('DropDown', function () { expect(this.renderableList.innerHTML).toBe('01'); }); - describe('if no data argument is passed' , function () { + describe('if no data argument is passed', function () { beforeEach(function () { this.data.map.calls.reset(); this.dropdown.renderChildren.calls.reset(); @@ -446,14 +414,14 @@ describe('DropDown', function () { describe('renderChildren', function () { beforeEach(function () { this.templateString = 'templateString'; - this.dropdown = { setImagesSrc: () => {}, templateString: this.templateString }; + this.dropdown = { templateString: this.templateString }; this.data = { droplab_hidden: true }; this.html = 'html'; this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } }; spyOn(utils, 'template').and.returnValue(this.html); spyOn(document, 'createElement').and.returnValue(this.template); - spyOn(this.dropdown, 'setImagesSrc'); + spyOn(DropDown, 'setImagesSrc'); this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data); }); @@ -471,7 +439,7 @@ describe('DropDown', function () { }); it('should call .setImagesSrc with the template', function () { - expect(this.dropdown.setImagesSrc).toHaveBeenCalledWith(this.template); + expect(DropDown.setImagesSrc).toHaveBeenCalledWith(this.template); }); it('should set the template display to none', function () { @@ -496,12 +464,11 @@ describe('DropDown', function () { describe('setImagesSrc', function () { beforeEach(function () { - this.dropdown = {}; this.template = { querySelectorAll: () => {} }; spyOn(this.template, 'querySelectorAll').and.returnValue([]); - DropDown.prototype.setImagesSrc.call(this.dropdown, this.template); + DropDown.setImagesSrc(this.template); }); it('should call .querySelectorAll', function () { @@ -562,7 +529,7 @@ describe('DropDown', function () { describe('toggle', function () { beforeEach(function () { - this.hidden = true + this.hidden = true; this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} }; spyOn(this.dropdown, 'show'); @@ -577,7 +544,7 @@ describe('DropDown', function () { describe('if .hidden is false', function () { beforeEach(function () { - this.hidden = false + this.hidden = false; this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} }; spyOn(this.dropdown, 'show'); diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js index 8ebdcdd1404..75bf5f3d611 100644 --- a/spec/javascripts/droplab/hook_spec.js +++ b/spec/javascripts/droplab/hook_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - import Hook from '~/droplab/hook'; import * as dropdownSrc from '~/droplab/drop_down'; @@ -73,10 +71,4 @@ describe('Hook', function () { }); }); }); - - describe('addEvents', function () { - it('should exist', function () { - expect(Hook.prototype.hasOwnProperty('addEvents')).toBe(true); - }); - }); }); diff --git a/spec/javascripts/fixtures/balsamiq.rb b/spec/javascripts/fixtures/balsamiq.rb new file mode 100644 index 00000000000..b5372821bf5 --- /dev/null +++ b/spec/javascripts/fixtures/balsamiq.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe 'Balsamiq file', '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'balsamiq-project') } + + before(:all) do + clean_frontend_fixtures('blob/balsamiq/') + end + + it 'blob/balsamiq/test.bmpr' do |example| + blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr') + + store_frontend_fixture(blob.data.force_encoding('utf-8'), example.description) + end +end diff --git a/spec/javascripts/fixtures/balsamiq_viewer.html.haml b/spec/javascripts/fixtures/balsamiq_viewer.html.haml new file mode 100644 index 00000000000..18166ba4901 --- /dev/null +++ b/spec/javascripts/fixtures/balsamiq_viewer.html.haml @@ -0,0 +1 @@ +.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js new file mode 100644 index 00000000000..ee456869c53 --- /dev/null +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -0,0 +1,60 @@ +import Vue from 'vue'; +import '~/render_math'; +import '~/render_gfm'; +import issuableApp from '~/issue_show/components/app.vue'; +import issueShowData from '../mock_data'; + +const issueShowInterceptor = data => (request, next) => { + next(request.respondWith(JSON.stringify(data), { + status: 200, + headers: { + 'POLL-INTERVAL': 1, + }, + })); +}; + +describe('Issuable output', () => { + document.body.innerHTML = '<span id="task_status"></span>'; + + let vm; + + beforeEach(() => { + const IssuableDescriptionComponent = Vue.extend(issuableApp); + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + + vm = new IssuableDescriptionComponent({ + propsData: { + canUpdate: true, + endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', + issuableRef: '#1', + initialTitle: '', + initialDescriptionHtml: '', + initialDescriptionText: '', + }, + }).$mount(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); + }); + + it('should render a title/description and update title/description on update', (done) => { + setTimeout(() => { + expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); + expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>'); + expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>'); + expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description'); + + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); + + setTimeout(() => { + expect(document.querySelector('title').innerText).toContain('2 (#1)'); + expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); + expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>'); + expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js new file mode 100644 index 00000000000..408349cc42d --- /dev/null +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -0,0 +1,99 @@ +import Vue from 'vue'; +import descriptionComponent from '~/issue_show/components/description.vue'; + +describe('Description component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(descriptionComponent); + + if (!document.querySelector('.issuable-meta')) { + const metaData = document.createElement('div'); + metaData.classList.add('issuable-meta'); + metaData.innerHTML = '<span id="task_status"></span><span id="task_status_short"></span>'; + + document.body.appendChild(metaData); + } + + vm = new Component({ + propsData: { + canUpdate: true, + descriptionHtml: 'test', + descriptionText: 'test', + updatedAt: new Date().toString(), + taskStatus: '', + }, + }).$mount(); + }); + + it('animates description changes', (done) => { + vm.descriptionHtml = 'changed'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + + setTimeout(() => { + expect( + vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'), + ).toBeTruthy(); + + done(); + }); + }); + }); + + it('re-inits the TaskList when description changed', (done) => { + spyOn(gl, 'TaskList'); + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect( + gl.TaskList, + ).toHaveBeenCalled(); + + done(); + }); + }); + + it('does not re-init the TaskList when canUpdate is false', (done) => { + spyOn(gl, 'TaskList'); + vm.canUpdate = false; + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect( + gl.TaskList, + ).not.toHaveBeenCalled(); + + done(); + }); + }); + + describe('taskStatus', () => { + it('adds full taskStatus', (done) => { + vm.taskStatus = '1 of 1'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status').textContent.trim(), + ).toBe('1 of 1'); + + done(); + }); + }); + + it('adds short taskStatus', (done) => { + vm.taskStatus = '1 of 1'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status_short').textContent.trim(), + ).toBe('1/1 task'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js new file mode 100644 index 00000000000..2f953e7e92e --- /dev/null +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -0,0 +1,67 @@ +import Vue from 'vue'; +import titleComponent from '~/issue_show/components/title.vue'; + +describe('Title component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(titleComponent); + vm = new Component({ + propsData: { + issuableRef: '#1', + titleHtml: 'Testing <img />', + titleText: 'Testing', + }, + }).$mount(); + }); + + it('renders title HTML', () => { + expect( + vm.$el.innerHTML.trim(), + ).toBe('Testing <img>'); + }); + + it('updates page title when changing titleHtml', (done) => { + spyOn(vm, 'setPageTitle'); + vm.titleHtml = 'test'; + + Vue.nextTick(() => { + expect( + vm.setPageTitle, + ).toHaveBeenCalled(); + + done(); + }); + }); + + it('animates title changes', (done) => { + vm.titleHtml = 'test'; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + + setTimeout(() => { + expect( + vm.$el.classList.contains('issue-realtime-trigger-pulse'), + ).toBeTruthy(); + + done(); + }); + }); + }); + + it('updates page title after changing title', (done) => { + vm.titleHtml = 'changed'; + vm.titleText = 'changed'; + + Vue.nextTick(() => { + expect( + document.querySelector('title').textContent.trim(), + ).toContain('changed'); + + done(); + }); + }); +}); diff --git a/spec/javascripts/issue_show/issue_title_description_spec.js b/spec/javascripts/issue_show/issue_title_description_spec.js deleted file mode 100644 index 1ec4fe58b08..00000000000 --- a/spec/javascripts/issue_show/issue_title_description_spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import Vue from 'vue'; -import $ from 'jquery'; -import '~/render_math'; -import '~/render_gfm'; -import issueTitleDescription from '~/issue_show/issue_title_description.vue'; -import issueShowData from './mock_data'; - -window.$ = $; - -const issueShowInterceptor = data => (request, next) => { - next(request.respondWith(JSON.stringify(data), { - status: 200, - headers: { - 'POLL-INTERVAL': 1, - }, - })); -}; - -describe('Issue Title', () => { - document.body.innerHTML = '<span id="task_status"></span>'; - - let IssueTitleDescriptionComponent; - - beforeEach(() => { - IssueTitleDescriptionComponent = Vue.extend(issueTitleDescription); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); - }); - - it('should render a title/description and update title/description on update', (done) => { - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); - - const issueShowComponent = new IssueTitleDescriptionComponent({ - propsData: { - canUpdateIssue: '.css-stuff', - endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title', - }, - }).$mount(); - - setTimeout(() => { - expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); - expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>'); - expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>'); - expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('this is a description'); - - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); - - setTimeout(() => { - expect(document.querySelector('title').innerText).toContain('2 (#1)'); - expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); - expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>'); - expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('42'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js index ad5a7b63470..6683d581bc5 100644 --- a/spec/javascripts/issue_show/mock_data.js +++ b/spec/javascripts/issue_show/mock_data.js @@ -4,23 +4,23 @@ export default { title_text: 'this is a title', description: '<p>this is a description!</p>', description_text: 'this is a description', - issue_number: 1, task_status: '2 of 4 completed', + updated_at: new Date().toString(), }, secondRequest: { title: '<p>2</p>', title_text: '2', description: '<p>42</p>', description_text: '42', - issue_number: 1, task_status: '0 of 0 completed', + updated_at: new Date().toString(), }, issueSpecRequest: { title: '<p>this is a title</p>', title_text: 'this is a title', description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>', description_text: '- [ ] Task List Item', - issue_number: 1, task_status: '0 of 1 completed', + updated_at: new Date().toString(), }, }; diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 918b6d32c43..22f30191ab9 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -1,9 +1,5 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; import Poll from '~/lib/utils/poll'; -Vue.use(VueResource); - const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { const timer = () => { setTimeout(() => { @@ -12,45 +8,33 @@ const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { } else { timer(); } - }, 5); + }, 0); }; timer(); }; -class ServiceMock { - constructor(endpoint) { - this.service = Vue.resource(endpoint); - } +function mockServiceCall(service, response, shouldFail = false) { + const action = shouldFail ? Promise.reject : Promise.resolve; + const responseObject = response; + + if (!responseObject.headers) responseObject.headers = {}; - fetch() { - return this.service.get(); - } + service.fetch.and.callFake(action.bind(Promise, responseObject)); } describe('Poll', () => { - let callbacks; - let service; + const service = jasmine.createSpyObj('service', ['fetch']); + const callbacks = jasmine.createSpyObj('callbacks', ['success', 'error']); - beforeEach(() => { - callbacks = { - success: () => {}, - error: () => {}, - }; - - service = new ServiceMock('endpoint'); - - spyOn(callbacks, 'success'); - spyOn(callbacks, 'error'); - spyOn(service, 'fetch').and.callThrough(); + afterEach(() => { + callbacks.success.calls.reset(); + callbacks.error.calls.reset(); + service.fetch.calls.reset(); }); it('calls the success callback when no header for interval is provided', (done) => { - const successInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200 })); - }; - - Vue.http.interceptors.push(successInterceptor); + mockServiceCall(service, { status: 200 }); new Poll({ resource: service, @@ -63,18 +47,12 @@ describe('Poll', () => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor); - done(); - }, 0); + }); }); it('calls the error callback whe the http request returns an error', (done) => { - const errorInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 500 })); - }; - - Vue.http.interceptors.push(errorInterceptor); + mockServiceCall(service, { status: 500 }, true); new Poll({ resource: service, @@ -86,42 +64,29 @@ describe('Poll', () => { waitForAllCallsToFinish(service, 1, () => { expect(callbacks.success).not.toHaveBeenCalled(); expect(callbacks.error).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor); done(); }); }); it('should call the success callback when the interval header is -1', (done) => { - const intervalInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } })); - }; - - Vue.http.interceptors.push(intervalInterceptor); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': -1 } }); new Poll({ resource: service, method: 'fetch', successCallback: callbacks.success, errorCallback: callbacks.error, - }).makeRequest(); - - setTimeout(() => { + }).makeRequest().then(() => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor); - done(); - }, 0); + }).catch(done.fail); }); it('starts polling when http status is 200 and interval header is provided', (done) => { - const pollInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); - }; - - Vue.http.interceptors.push(pollInterceptor); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); const Polling = new Poll({ resource: service, @@ -141,19 +106,13 @@ describe('Poll', () => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); - done(); }); }); describe('stop', () => { it('stops polling when method is called', (done) => { - const pollInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); - }; - - Vue.http.interceptors.push(pollInterceptor); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); const Polling = new Poll({ resource: service, @@ -174,8 +133,6 @@ describe('Poll', () => { expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); expect(Polling.stop).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); - done(); }); }); @@ -183,11 +140,7 @@ describe('Poll', () => { describe('restart', () => { it('should restart polling when its called', (done) => { - const pollInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); - }; - - Vue.http.interceptors.push(pollInterceptor); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); const Polling = new Poll({ resource: service, @@ -215,8 +168,6 @@ describe('Poll', () => { expect(Polling.stop).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); - done(); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index be4605a5b89..8243a9c991a 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -377,7 +377,7 @@ import '~/notes'; }); it('should return true when comment begins with a slash command', () => { - const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); expect(hasSlashCommands).toBeTruthy(); @@ -401,10 +401,18 @@ import '~/notes'; describe('stripSlashCommands', () => { it('should strip slash commands from the comment which begins with a slash command', () => { this.notes = new Notes(); - const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const stripedComment = this.notes.stripSlashCommands(sampleComment); - expect(stripedComment).not.toBe(sampleComment); + expect(stripedComment).toBe(''); + }); + + it('should strip slash commands from the comment but leaves plain comment if it is present', () => { + this.notes = new Notes(); + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; + const stripedComment = this.notes.stripSlashCommands(sampleComment); + + expect(stripedComment).toBe('Merging this'); }); it('should NOT strip string that has slashes within', () => { @@ -424,6 +432,22 @@ import '~/notes'; beforeEach(() => { this.notes = new Notes('', []); + spyOn(_, 'escape').and.callFake((comment) => { + const escapedString = comment.replace(/["&'<>]/g, (a) => { + const escapedToken = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }[a]; + + return escapedToken; + }); + + return escapedString; + }); }); it('should return constructed placeholder element for regular note based on form contents', () => { @@ -444,7 +468,21 @@ import '~/notes'; expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname); expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`); - expect($tempNote.find('.note-body .note-text').text().trim()).toEqual(sampleComment); + expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment); + }); + + it('should escape HTML characters from note based on form contents', () => { + const commentWithHtml = '<script>alert("Boom!");</script>'; + const $tempNote = this.notes.createPlaceholderNote({ + formContent: commentWithHtml, + uniqueId, + isDiscussionNote: false, + currentUsername, + currentUserFullname + }); + + expect(_.escape).toHaveBeenCalledWith(commentWithHtml); + expect($tempNote.find('.note-body .note-text p').html()).toEqual('<script>alert("Boom!");</script>'); }); it('should return constructed placeholder element for discussion note based on form contents', () => { 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 index 2f971b39d16..d4b200875df 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -1,7 +1,7 @@ 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'; +import { statusIconEntityMap } from '~/vue_shared/ci_status_icons'; const deploymentMockData = [ { @@ -46,7 +46,7 @@ describe('MRWidgetDeployment', () => { describe('svg', () => { it('should have the proper SVG icon', () => { const vm = createComponent(deploymentMockData); - expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success); + expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success); }); }); }); 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 index 48f816c8460..7f3eea7d2e5 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -48,10 +48,12 @@ describe('MRWidgetHeader', () => { describe('template', () => { let vm; let el; + const sourceBranchPath = '/foo/bar/mr-widget-refactor'; const mr = { divergedCommitsCount: 12, sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '/foo/bar/mr-widget-refactor', + sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, + targetBranchPath: 'foo/bar/commits-path', targetBranch: 'master', isOpen: true, emailPatchesPath: '/mr/email-patches', @@ -65,8 +67,13 @@ describe('MRWidgetHeader', () => { 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); + const sourceBranchLink = el.querySelectorAll('.label-branch')[0]; + const targetBranchLink = el.querySelectorAll('.label-branch')[1]; + + expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); + expect(targetBranchLink.textContent).toContain(mr.targetBranch); + expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); + expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind'); expect(el.textContent).toContain('Check out branch'); 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 index 1b418c7dfcf..647b59520f8 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons'; +import { statusIconEntityMap } from '~/vue_shared/ci_status_icons'; import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline'; import mockData from '../mock_data'; @@ -24,7 +24,7 @@ describe('MRWidgetPipeline', () => { describe('components', () => { it('should have components added', () => { expect(pipelineComponent.components['pipeline-stage']).toBeDefined(); - expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined(); + expect(pipelineComponent.components.ciIcon).toBeDefined(); }); }); @@ -33,7 +33,7 @@ describe('MRWidgetPipeline', () => { it('should have the proper SVG icon', () => { const vm = createComponent({ pipeline: mockData.pipeline }); - expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed); + expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed); }); }); 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 index 78a70725e94..47303d1e80f 100644 --- 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 @@ -3,7 +3,7 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid const mr = { targetBranch: 'good-branch', - targetBranchCommitsPath: '/good-branch', + targetBranchPath: '/good-branch', closedBy: { name: 'Fatih Acet', username: 'fatihacet', @@ -44,7 +44,7 @@ describe('MRWidgetClosed', () => { 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').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js new file mode 100644 index 00000000000..5fb1d69a8b3 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch'; + +describe('MRWidgetSHAMismatch', () => { + describe('template', () => { + const Component = Vue.extend(shaMismatchComponent); + const vm = new Component({ + el: document.createElement('div'), + }); + it('should have correct elements', () => { + expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); + expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging.'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js index ee944f4d4e5..9a331d99865 100644 --- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js @@ -25,6 +25,9 @@ describe('getStateKey', () => { context.canBeMerged = true; expect(bound()).toEqual('readyToMerge'); + context.hasSHAChanged = true; + expect(bound()).toEqual('shaMismatch'); + context.isPipelineBlocked = true; expect(bound()).toEqual('pipelineBlocked'); diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js new file mode 100644 index 00000000000..56dd0198ae2 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js @@ -0,0 +1,22 @@ +import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; +import mockData from '../mock_data'; + +describe('MergeRequestStore', () => { + describe('setData', () => { + let store; + + beforeEach(() => { + store = new MergeRequestStore(mockData); + }); + + it('should set hasSHAChanged when the diff SHA changes', () => { + store.setData({ ...mockData, diff_head_sha: 'a-different-string' }); + expect(store.hasSHAChanged).toBe(true); + }); + + it('should not set hasSHAChanged when other data changes', () => { + store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); + expect(store.hasSHAChanged).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js new file mode 100644 index 00000000000..daed4da3e15 --- /dev/null +++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js @@ -0,0 +1,89 @@ +import Vue from 'vue'; +import ciBadge from '~/vue_shared/components/ci_badge_link.vue'; + +describe('CI Badge Link Component', () => { + let CIBadge; + + const statuses = { + canceled: { + text: 'canceled', + label: 'canceled', + group: 'canceled', + icon: 'icon_status_canceled', + details_path: 'status/canceled', + }, + created: { + text: 'created', + label: 'created', + group: 'created', + icon: 'icon_status_created', + details_path: 'status/created', + }, + failed: { + text: 'failed', + label: 'failed', + group: 'failed', + icon: 'icon_status_failed', + details_path: 'status/failed', + }, + manual: { + text: 'manual', + label: 'manual action', + group: 'manual', + icon: 'icon_status_manual', + details_path: 'status/manual', + }, + pending: { + text: 'pending', + label: 'pending', + group: 'pending', + icon: 'icon_status_pending', + details_path: 'status/pending', + }, + running: { + text: 'running', + label: 'running', + group: 'running', + icon: 'icon_status_running', + details_path: 'status/running', + }, + skipped: { + text: 'skipped', + label: 'skipped', + group: 'skipped', + icon: 'icon_status_skipped', + details_path: 'status/skipped', + }, + success_warining: { + text: 'passed', + label: 'passed', + group: 'success_with_warnings', + icon: 'icon_status_warning', + details_path: 'status/warning', + }, + success: { + text: 'passed', + label: 'passed', + group: 'passed', + icon: 'icon_status_success', + details_path: 'status/passed', + }, + }; + + it('should render each status badge', () => { + CIBadge = Vue.extend(ciBadge); + Object.keys(statuses).map((status) => { + const vm = new CIBadge({ + propsData: { + status: statuses[status], + }, + }).$mount(); + + expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path); + expect(vm.$el.textContent.trim()).toEqual(statuses[status].text); + expect(vm.$el.getAttribute('class')).toEqual(`ci-status ci-${statuses[status].group}`); + expect(vm.$el.querySelector('svg')).toBeDefined(); + return vm; + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js index 98dc6caa622..d8664408595 100644 --- a/spec/javascripts/vue_shared/components/ci_icon_spec.js +++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js @@ -25,6 +25,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_success', + group: 'success', }, }, }).$mount(); @@ -37,6 +38,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_failed', + group: 'failed', }, }, }).$mount(); @@ -49,6 +51,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_warning', + group: 'warning', }, }, }).$mount(); @@ -61,6 +64,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_pending', + group: 'pending', }, }, }).$mount(); @@ -73,6 +77,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_running', + group: 'running', }, }, }).$mount(); @@ -85,6 +90,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_created', + group: 'created', }, }, }).$mount(); @@ -97,6 +103,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_skipped', + group: 'skipped', }, }, }).$mount(); @@ -109,6 +116,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_canceled', + group: 'canceled', }, }, }).$mount(); @@ -121,6 +129,7 @@ describe('CI Icon component', () => { propsData: { status: { icon: 'icon_status_manual', + group: 'manual', }, }, }).$mount(); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index df547299d75..242010ba688 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -61,16 +61,16 @@ describe('Commit component', () => { }); it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); + expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(props.commitRef.ref_url); }); it('should render the ref name', () => { - expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); + expect(component.$el.querySelector('.ref-name').textContent).toContain(props.commitRef.name); }); it('should render the commit short sha with a link to the commit url', () => { - expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl); - expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha); + expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(props.commitUrl); + expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha); }); it('should render the given commitIconSvg', () => { diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 0a2c66e72d7..14280751053 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -89,7 +89,7 @@ describe('Pipelines Table Row', () => { it('should render link to commit', () => { component = buildComponent(pipeline); - const commitLink = component.$el.querySelector('.branch-commit .commit-id'); + const commitLink = component.$el.querySelector('.branch-commit .commit-sha'); expect(commitLink.getAttribute('href')).toEqual(pipeline.commit.commit_path); }); |