diff options
Diffstat (limited to 'spec/javascripts')
29 files changed, 1448 insertions, 208 deletions
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 8ec96bdb583..461908f3fde 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -8,13 +8,12 @@ import '~/breakpoints'; import 'vendor/jquery.nicescroll'; describe('Build', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; preloadFixtures('builds/build-with-artifacts.html.raw'); beforeEach(() => { loadFixtures('builds/build-with-artifacts.html.raw'); - spyOn($, 'ajax'); }); describe('class constructor', () => { @@ -33,7 +32,6 @@ describe('Build', () => { it('copies build options', function () { expect(this.build.pageUrl).toBe(BUILD_URL); - expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`); expect(this.build.buildStatus).toBe('success'); expect(this.build.buildStage).toBe('test'); expect(this.build.state).toBe(''); @@ -65,27 +63,14 @@ describe('Build', () => { }); describe('running build', () => { - beforeEach(function () { - this.build = new Build(); - }); - it('updates the build trace on an interval', function () { + const deferred1 = $.Deferred(); + const deferred2 = $.Deferred(); + const deferred3 = $.Deferred(); + spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); spyOn(gl.utils, 'visitUrl'); - jasmine.clock().tick(4001); - - expect($.ajax.calls.count()).toBe(1); - - // We have to do it this way to prevent Webpack to fail to compile - // when destructuring assignments and reusing - // the same variables names inside the same scope - let args = $.ajax.calls.argsFor(0)[0]; - - expect(args.url).toBe(`${BUILD_URL}/trace.json`); - expect(args.dataType).toBe('json'); - expect(args.success).toEqual(jasmine.any(Function)); - - args.success.call($, { + deferred1.resolve({ html: '<span>Update<span>', status: 'running', state: 'newstate', @@ -93,20 +78,9 @@ describe('Build', () => { complete: false, }); - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.build.state).toBe('newstate'); - - jasmine.clock().tick(4001); - - expect($.ajax.calls.count()).toBe(3); - - args = $.ajax.calls.argsFor(2)[0]; - expect(args.url).toBe(`${BUILD_URL}/trace.json`); - expect(args.dataType).toBe('json'); - expect(args.data.state).toBe('newstate'); - expect(args.success).toEqual(jasmine.any(Function)); + deferred2.resolve(); - args.success.call($, { + deferred3.resolve({ html: '<span>More</span>', status: 'running', state: 'finalstate', @@ -114,150 +88,222 @@ describe('Build', () => { complete: true, }); + this.build = new Build(); + + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + expect(this.build.state).toBe('newstate'); + + jasmine.clock().tick(4001); + expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); expect(this.build.state).toBe('finalstate'); }); it('replaces the entire build trace', () => { + const deferred1 = $.Deferred(); + const deferred2 = $.Deferred(); + const deferred3 = $.Deferred(); + + spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); + spyOn(gl.utils, 'visitUrl'); - jasmine.clock().tick(4001); - let args = $.ajax.calls.argsFor(0)[0]; - args.success.call($, { - html: '<span>Update</span>', + deferred1.resolve({ + html: '<span>Update<span>', status: 'running', append: false, complete: false, }); - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + deferred2.resolve(); - jasmine.clock().tick(4001); - args = $.ajax.calls.argsFor(2)[0]; - args.success.call($, { + deferred3.resolve({ html: '<span>Different</span>', status: 'running', append: false, }); + this.build = new Build(); + + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + + jasmine.clock().tick(4001); + expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); expect($('#build-trace .js-build-output').text()).toMatch(/Different/); }); it('reloads the page when the build is done', () => { spyOn(gl.utils, 'visitUrl'); + const deferred = $.Deferred(); - jasmine.clock().tick(4001); - const [{ success }] = $.ajax.calls.argsFor(0); - success.call($, { + spyOn($, 'ajax').and.returnValue(deferred.promise()); + deferred.resolve({ html: '<span>Final</span>', status: 'passed', append: true, complete: true, }); + this.build = new Build(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); }); + }); - describe('truncated information', () => { - describe('when size is less than total', () => { - it('shows information about truncated log', () => { - jasmine.clock().tick(4001); - const [{ success }] = $.ajax.calls.argsFor(0); - - success.call($, { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 50, - total: 100, - }); - - expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + describe('truncated information', () => { + describe('when size is less than total', () => { + it('shows information about truncated log', () => { + spyOn(gl.utils, 'visitUrl'); + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + + deferred.resolve({ + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, }); - it('shows the size in KiB', () => { - jasmine.clock().tick(4001); - const [{ success }] = $.ajax.calls.argsFor(0); - const size = 50; - - success.call($, { - html: '<span>Update</span>', - status: 'success', - append: false, - size, - total: 100, - }); - - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${bytesToKiB(size)}`); + this.build = new Build(); + + expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + }); + + it('shows the size in KiB', () => { + const size = 50; + spyOn(gl.utils, 'visitUrl'); + const deferred = $.Deferred(); + + spyOn($, 'ajax').and.returnValue(deferred.promise()); + deferred.resolve({ + html: '<span>Update</span>', + status: 'success', + append: false, + size, + total: 100, }); - it('shows incremented size', () => { - jasmine.clock().tick(4001); - let args = $.ajax.calls.argsFor(0)[0]; - args.success.call($, { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 50, - total: 100, - }); - - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${bytesToKiB(50)}`); - - jasmine.clock().tick(4001); - args = $.ajax.calls.argsFor(2)[0]; - args.success.call($, { - html: '<span>Update</span>', - status: 'success', - append: true, - size: 10, - total: 100, - }); - - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${bytesToKiB(60)}`); + this.build = new Build(); + + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${bytesToKiB(size)}`); + }); + + it('shows incremented size', () => { + const deferred1 = $.Deferred(); + const deferred2 = $.Deferred(); + const deferred3 = $.Deferred(); + + spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); + + spyOn(gl.utils, 'visitUrl'); + + deferred1.resolve({ + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, }); - it('renders the raw link', () => { - jasmine.clock().tick(4001); - const [{ success }] = $.ajax.calls.argsFor(0); - - success.call($, { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 50, - total: 100, - }); - - expect( - document.querySelector('.js-raw-link').textContent.trim(), - ).toContain('Complete Raw'); + deferred2.resolve(); + + this.build = new Build(); + + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${bytesToKiB(50)}`); + + jasmine.clock().tick(4001); + + deferred3.resolve({ + html: '<span>Update</span>', + status: 'success', + append: true, + size: 10, + total: 100, }); + + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${bytesToKiB(60)}`); }); - describe('when size is equal than total', () => { - it('does not show the trunctated information', () => { - jasmine.clock().tick(4001); - const [{ success }] = $.ajax.calls.argsFor(0); + it('renders the raw link', () => { + const deferred = $.Deferred(); + spyOn(gl.utils, 'visitUrl'); + + spyOn($, 'ajax').and.returnValue(deferred.promise()); + deferred.resolve({ + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, + }); - success.call($, { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 100, - total: 100, - }); + this.build = new Build(); - expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + expect( + document.querySelector('.js-raw-link').textContent.trim(), + ).toContain('Complete Raw'); + }); + }); + + describe('when size is equal than total', () => { + it('does not show the trunctated information', () => { + const deferred = $.Deferred(); + spyOn(gl.utils, 'visitUrl'); + + spyOn($, 'ajax').and.returnValue(deferred.promise()); + deferred.resolve({ + html: '<span>Update</span>', + status: 'success', + append: false, + size: 100, + total: 100, }); + + this.build = new Build(); + + expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + }); + }); + }); + + describe('output trace', () => { + beforeEach(() => { + const deferred = $.Deferred(); + spyOn(gl.utils, 'visitUrl'); + + spyOn($, 'ajax').and.returnValue(deferred.promise()); + deferred.resolve({ + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, }); + + this.build = new Build(); + }); + + it('should render trace controls', () => { + const controllers = document.querySelector('.controllers'); + + expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined(); + expect(controllers.querySelector('.js-erase-link')).toBeDefined(); + expect(controllers.querySelector('.js-scroll-up')).toBeDefined(); + expect(controllers.querySelector('.js-scroll-down')).toBeDefined(); + }); + + it('should render received output', () => { + expect( + document.querySelector('.js-build-output').innerHTML, + ).toEqual('<span>Update</span>'); }); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 8688332782d..6e59ee96c6b 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -57,6 +57,7 @@ describe('Filtered Search Manager', () => { input = document.querySelector('.filtered-search'); tokensContainer = document.querySelector('.tokens-container'); manager = new gl.FilteredSearchManager(); + manager.setup(); }); afterEach(() => { @@ -72,6 +73,7 @@ describe('Filtered Search Manager', () => { spyOn(recentSearchesStoreSrc, 'default'); filteredSearchManager = new gl.FilteredSearchManager(); + filteredSearchManager.setup(); return filteredSearchManager; }); @@ -89,6 +91,7 @@ describe('Filtered Search Manager', () => { spyOn(window, 'Flash'); filteredSearchManager = new gl.FilteredSearchManager(); + filteredSearchManager.setup(); expect(window.Flash).not.toHaveBeenCalled(); }); diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/jobs.rb index 320de791b08..dc7dde1138c 100644 --- a/spec/javascripts/fixtures/builds.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do +describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index ee456869c53..0030a953119 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import '~/render_math'; import '~/render_gfm'; import issuableApp from '~/issue_show/components/app.vue'; +import eventHub from '~/issue_show/event_hub'; import issueShowData from '../mock_data'; const issueShowInterceptor = data => (request, next) => { @@ -22,14 +23,25 @@ describe('Issuable output', () => { const IssuableDescriptionComponent = Vue.extend(issuableApp); Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + spyOn(eventHub, '$emit'); + vm = new IssuableDescriptionComponent({ propsData: { canUpdate: true, + canDestroy: true, + canMove: true, endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', issuableRef: '#1', - initialTitle: '', + initialTitleHtml: '', + initialTitleText: '', initialDescriptionHtml: '', initialDescriptionText: '', + markdownPreviewUrl: '/', + markdownDocs: '/', + projectsAutocompleteUrl: '/', + isConfidential: false, + projectNamespace: '/', + projectPath: '/', }, }).$mount(); }); @@ -57,4 +69,296 @@ describe('Issuable output', () => { }); }); }); + + it('shows actions if permissions are correct', (done) => { + vm.showForm = true; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn'), + ).not.toBeNull(); + + done(); + }); + }); + + it('does not show actions if permissions are incorrect', (done) => { + vm.showForm = true; + vm.canUpdate = false; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn'), + ).toBeNull(); + + done(); + }); + }); + + it('does not update formState if form is already open', (done) => { + vm.openForm(); + + vm.state.titleText = 'testing 123'; + + vm.openForm(); + + Vue.nextTick(() => { + expect( + vm.store.formState.title, + ).not.toBe('testing 123'); + + done(); + }); + }); + + describe('updateIssuable', () => { + it('fetches new data after update', (done) => { + spyOn(vm.service, 'getData'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + confidential: false, + web_url: location.pathname, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + vm.service.getData, + ).toHaveBeenCalled(); + + done(); + }); + }); + + it('reloads the page if the confidential status has changed', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + confidential: true, + web_url: location.pathname, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).toHaveBeenCalledWith(location.pathname); + + done(); + }); + }); + + it('correctly updates issuable data', (done) => { + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve(); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + vm.service.updateIssuable, + ).toHaveBeenCalledWith(vm.formState); + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + + done(); + }); + }); + + it('does not redirect if issue has not moved', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + web_url: location.pathname, + confidential: vm.isConfidential, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('redirects if issue is moved', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + web_url: '/testing-issue-move', + confidential: vm.isConfidential, + }; + }, + }); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).toHaveBeenCalledWith('/testing-issue-move'); + + done(); + }); + }); + + it('does not update issuable if project move confirm is false', (done) => { + spyOn(window, 'confirm').and.returnValue(false); + spyOn(vm.service, 'updateIssuable'); + + vm.store.formState.move_to_project_id = 1; + + vm.updateIssuable(); + + setTimeout(() => { + expect( + vm.service.updateIssuable, + ).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('closes form on error', (done) => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + + vm.updateIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating issue'); + + done(); + }); + }); + }); + + describe('deleteIssuable', () => { + it('changes URL when deleted', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { web_url: '/test' }; + }, + }); + })); + + vm.deleteIssuable(); + + setTimeout(() => { + expect( + gl.utils.visitUrl, + ).toHaveBeenCalledWith('/test'); + + done(); + }); + }); + + it('stops polling when deleting', (done) => { + spyOn(gl.utils, 'visitUrl'); + spyOn(vm.poll, 'stop'); + spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { web_url: '/test' }; + }, + }); + })); + + vm.deleteIssuable(); + + setTimeout(() => { + expect( + vm.poll.stop, + ).toHaveBeenCalledWith(); + + done(); + }); + }); + + it('closes form on error', (done) => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + + vm.deleteIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error deleting issue'); + + done(); + }); + }); + }); + + describe('open form', () => { + it('shows locked warning if form is open & data is different', (done) => { + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + + Vue.nextTick() + .then(() => new Promise((resolve) => { + setTimeout(resolve); + })) + .then(() => { + vm.openForm(); + + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); + + return new Promise((resolve) => { + setTimeout(resolve); + }); + }) + .then(() => { + expect( + vm.formState.lockedWarningVisible, + ).toBeTruthy(); + + expect( + vm.$el.querySelector('.alert'), + ).not.toBeNull(); + + done(); + }) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js new file mode 100644 index 00000000000..f6625b748b6 --- /dev/null +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -0,0 +1,147 @@ +import Vue from 'vue'; +import editActions from '~/issue_show/components/edit_actions.vue'; +import eventHub from '~/issue_show/event_hub'; +import Store from '~/issue_show/stores'; + +describe('Edit Actions components', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(editActions); + const store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.title = 'test'; + + spyOn(eventHub, '$emit'); + + vm = new Component({ + propsData: { + canDestroy: true, + formState: store.formState, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders all buttons as enabled', () => { + expect( + vm.$el.querySelectorAll('.disabled').length, + ).toBe(0); + + expect( + vm.$el.querySelectorAll('[disabled]').length, + ).toBe(0); + }); + + it('does not render delete button if canUpdate is false', (done) => { + vm.canDestroy = false; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-danger'), + ).toBeNull(); + + done(); + }); + }); + + it('disables submit button when title is blank', (done) => { + vm.formState.title = ''; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-save').getAttribute('disabled'), + ).toBe('disabled'); + + done(); + }); + }); + + describe('updateIssuable', () => { + it('sends update.issauble event when clicking save button', () => { + vm.$el.querySelector('.btn-save').click(); + + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('update.issuable'); + }); + + it('shows loading icon after clicking save button', (done) => { + vm.$el.querySelector('.btn-save').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-save .fa'), + ).not.toBeNull(); + + done(); + }); + }); + + it('disabled button after clicking save button', (done) => { + vm.$el.querySelector('.btn-save').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-save').getAttribute('disabled'), + ).toBe('disabled'); + + done(); + }); + }); + }); + + describe('closeForm', () => { + it('emits close.form when clicking cancel', () => { + vm.$el.querySelector('.btn-default').click(); + + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + }); + }); + + describe('deleteIssuable', () => { + it('sends delete.issuable event when clicking save button', () => { + spyOn(window, 'confirm').and.returnValue(true); + vm.$el.querySelector('.btn-danger').click(); + + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('delete.issuable'); + }); + + it('shows loading icon after clicking delete button', (done) => { + spyOn(window, 'confirm').and.returnValue(true); + vm.$el.querySelector('.btn-danger').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.btn-danger .fa'), + ).not.toBeNull(); + + done(); + }); + }); + + it('does no actions when confirm is false', (done) => { + spyOn(window, 'confirm').and.returnValue(false); + vm.$el.querySelector('.btn-danger').click(); + + Vue.nextTick(() => { + expect( + eventHub.$emit, + ).not.toHaveBeenCalledWith('delete.issuable'); + expect( + vm.$el.querySelector('.btn-danger .fa'), + ).toBeNull(); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js new file mode 100644 index 00000000000..f5b35b1e8b0 --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/description_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import Store from '~/issue_show/stores'; +import descriptionField from '~/issue_show/components/fields/description.vue'; + +describe('Description field component', () => { + let vm; + let store; + + beforeEach((done) => { + const Component = Vue.extend(descriptionField); + const el = document.createElement('div'); + store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.description = 'test'; + + document.body.appendChild(el); + + vm = new Component({ + el, + propsData: { + markdownPreviewUrl: '/', + markdownDocs: '/', + formState: store.formState, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders markdown field with description', () => { + expect( + vm.$el.querySelector('.md-area textarea').value, + ).toBe('test'); + }); + + it('renders markdown field with a markdown description', (done) => { + store.formState.description = '**test**'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.md-area textarea').value, + ).toBe('**test**'); + + done(); + }); + }); + + it('focuses field when mounted', () => { + expect( + document.activeElement, + ).toBe(vm.$refs.textarea); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js new file mode 100644 index 00000000000..2b7ee65094b --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; +import '~/templates/issuable_template_selector'; +import '~/templates/issuable_template_selectors'; + +describe('Issue description template component', () => { + let vm; + let formState; + + beforeEach((done) => { + const Component = Vue.extend(descriptionTemplate); + formState = { + description: 'test', + }; + + vm = new Component({ + propsData: { + formState, + issuableTemplates: [{ name: 'test' }], + projectPath: '/', + projectNamespace: '/', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders templates as JSON array in data attribute', () => { + expect( + vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data'), + ).toBe('[{"name":"test"}]'); + }); + + it('updates formState when changing template', () => { + vm.issuableTemplate.editor.setValue('test new template'); + + expect( + formState.description, + ).toBe('test new template'); + }); + + it('returns formState description with editor getValue', () => { + formState.description = 'testing new template'; + + expect( + vm.issuableTemplate.editor.getValue(), + ).toBe('testing new template'); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js new file mode 100644 index 00000000000..86d35c33ff4 --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import projectMove from '~/issue_show/components/fields/project_move.vue'; + +describe('Project move field component', () => { + let vm; + let formState; + + beforeEach((done) => { + const Component = Vue.extend(projectMove); + + formState = { + move_to_project_id: 0, + }; + + vm = new Component({ + propsData: { + formState, + projectsAutocompleteUrl: '/autocomplete', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('mounts select2 element', () => { + expect( + vm.$el.querySelector('.select2-container'), + ).not.toBeNull(); + }); + + it('updates formState on change', () => { + $(vm.$refs['move-dropdown']).val(2).trigger('change'); + + expect( + formState.move_to_project_id, + ).toBe(2); + }); +}); diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js new file mode 100644 index 00000000000..53ae038a6a2 --- /dev/null +++ b/spec/javascripts/issue_show/components/fields/title_spec.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Store from '~/issue_show/stores'; +import titleField from '~/issue_show/components/fields/title.vue'; + +describe('Title field component', () => { + let vm; + let store; + + beforeEach(() => { + const Component = Vue.extend(titleField); + store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.title = 'test'; + + vm = new Component({ + propsData: { + formState: store.formState, + }, + }).$mount(); + }); + + it('renders form control with formState title', () => { + expect( + vm.$el.querySelector('.form-control').value, + ).toBe('test'); + }); +}); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js new file mode 100644 index 00000000000..9a85223208c --- /dev/null +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import formComponent from '~/issue_show/components/form.vue'; +import '~/templates/issuable_template_selector'; +import '~/templates/issuable_template_selectors'; + +describe('Inline edit form component', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(formComponent); + + vm = new Component({ + propsData: { + canDestroy: true, + canMove: true, + formState: { + title: 'b', + description: 'a', + lockedWarningVisible: false, + }, + markdownPreviewUrl: '/', + markdownDocs: '/', + projectsAutocompleteUrl: '/', + projectPath: '/', + projectNamespace: '/', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('does not render template selector if no templates exist', () => { + expect( + vm.$el.querySelector('.js-issuable-selector-wrap'), + ).toBeNull(); + }); + + it('renders template selector when templates exists', (done) => { + spyOn(gl, 'IssuableTemplateSelectors'); + vm.issuableTemplates = ['test']; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-issuable-selector-wrap'), + ).not.toBeNull(); + + done(); + }); + }); + + it('hides locked warning by default', () => { + expect( + vm.$el.querySelector('.alert'), + ).toBeNull(); + }); + + it('shows locked warning if formState is different', (done) => { + vm.formState.lockedWarningVisible = true; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.alert'), + ).not.toBeNull(); + + done(); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index 2f953e7e92e..a2d90a9b9f5 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import Store from '~/issue_show/stores'; import titleComponent from '~/issue_show/components/title.vue'; describe('Title component', () => { @@ -6,11 +7,18 @@ describe('Title component', () => { beforeEach(() => { const Component = Vue.extend(titleComponent); + const store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); vm = new Component({ propsData: { issuableRef: '#1', titleHtml: 'Testing <img />', titleText: 'Testing', + showForm: false, + formState: store.formState, }, }).$mount(); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index e9bffd74d90..e3938a77680 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -356,7 +356,7 @@ import '~/lib/utils/common_utils'; describe('gl.utils.setCiStatusFavicon', () => { it('should set page favicon to CI status favicon based on provided status', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`; + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; const FAVICON_PATH = '//icon_status_success'; const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index 90b12c9f115..83c92deccdc 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,4 @@ -import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils'; +import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { @@ -45,4 +45,11 @@ describe('Number Utils', () => { expect(bytesToKiB(1000)).toEqual(0.9765625); }); }); + + describe('bytesToMiB', () => { + it('calculates MiB for the given bytes', () => { + expect(bytesToMiB(1048576)).toEqual(1); + expect(bytesToMiB(1000000)).toEqual(0.95367431640625); + }); + }); }); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 1173fa40947..f444bcaf847 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -13,7 +13,9 @@ import '~/merge_request'; }); it('modifies the Markdown field', function() { spyOn(jQuery, 'ajax').and.stub(); - $('input[type=checkbox]').attr('checked', true).trigger('change'); + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', true, true); + $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); return it('submits an ajax request on tasklist:changed', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 3d1706aab68..7b910282cc8 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,4 +1,5 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ +/* global Notes */ import '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; @@ -7,6 +8,7 @@ import '~/lib/utils/common_utils'; import '~/diff'; import '~/single_file_diff'; import '~/files_comment_button'; +import '~/notes'; import 'vendor/jquery.scrollTo'; (function () { @@ -29,7 +31,7 @@ import 'vendor/jquery.scrollTo'; }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -286,8 +288,49 @@ import 'vendor/jquery.scrollTo'; spyOn($, 'ajax').and.callFake(function (options) { expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); }); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); }); + + describe('with note fragment hash', () => { + beforeEach(() => { + loadFixtures('merge_requests/diff_comment.html.raw'); + spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); + window.notes = new Notes('', []); + spyOn(window.notes, 'toggleDiffNote').and.callThrough(); + }); + + afterEach(() => { + delete window.notes; + }); + + it('should expand and scroll to linked fragment hash #note_xxx', function () { + const noteId = 'note_1'; + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: `<div id="${noteId}">foo</div>` }); + }); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); + }); + + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ html: '' }); + }); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); + }); }); }); }).call(window); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 025f08ee332..17aa70ff3f1 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -34,7 +34,9 @@ import '~/notes'; }); it('modifies the Markdown field', function() { - $('input[type=checkbox]').attr('checked', true).trigger('change'); + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', true, true); + $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); @@ -128,7 +130,6 @@ import '~/notes'; beforeEach(() => { note = { id: 1, - discussion_html: null, valid: true, note: 'heya', html: '<div>heya</div>', diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index f033956c071..85bd87318db 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -4,7 +4,7 @@ import actionComponent from '~/pipelines/components/graph/action_component.vue'; describe('pipeline graph action component', () => { let component; - beforeEach(() => { + beforeEach((done) => { const ActionComponent = Vue.extend(actionComponent); component = new ActionComponent({ propsData: { @@ -14,6 +14,8 @@ describe('pipeline graph action component', () => { actionIcon: 'icon_action_cancel', }, }).$mount(); + + Vue.nextTick(done); }); it('should render a link', () => { @@ -27,7 +29,7 @@ describe('pipeline graph action component', () => { it('should update bootstrap tooltip when title changes', (done) => { component.tooltipText = 'changed'; - Vue.nextTick(() => { + setTimeout(() => { expect(component.$el.getAttribute('data-original-title')).toBe('changed'); done(); }); diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js index 14ff1b0d25c..25fd18b197e 100644 --- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js +++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js @@ -4,7 +4,7 @@ import dropdownActionComponent from '~/pipelines/components/graph/dropdown_actio describe('action component', () => { let component; - beforeEach(() => { + beforeEach((done) => { const DropdownActionComponent = Vue.extend(dropdownActionComponent); component = new DropdownActionComponent({ propsData: { @@ -14,6 +14,8 @@ describe('action component', () => { actionIcon: 'icon_action_cancel', }, }).$mount(); + + Vue.nextTick(done); }); it('should render a link', () => { diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js index 6bd0eb86263..713baa65a17 100644 --- a/spec/javascripts/pipelines/graph/graph_component_spec.js +++ b/spec/javascripts/pipelines/graph/graph_component_spec.js @@ -14,49 +14,42 @@ describe('graph component', () => { describe('while is loading', () => { it('should render a loading icon', () => { - const component = new GraphComponent().$mount('#js-pipeline-graph-vue'); + const component = new GraphComponent({ + propsData: { + isLoading: true, + pipeline: {}, + }, + }).$mount('#js-pipeline-graph-vue'); expect(component.$el.querySelector('.loading-icon')).toBeDefined(); }); }); - describe('with a successfull response', () => { - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(graphJSON), { - status: 200, - })); - }; + describe('with data', () => { + it('should render the graph', () => { + const component = new GraphComponent({ + propsData: { + isLoading: false, + pipeline: graphJSON, + }, + }).$mount('#js-pipeline-graph-vue'); - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); - - it('should render the graph', (done) => { - const component = new GraphComponent().$mount('#js-pipeline-graph-vue'); - - setTimeout(() => { - expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true); + expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true); - expect( - component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'), - ).toEqual(true); + expect( + component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'), + ).toEqual(true); - expect( - component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'), - ).toEqual(true); + expect( + component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'), + ).toEqual(true); - expect( - component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), - ).toEqual(true); + expect( + component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), + ).toEqual(true); - expect(component.$el.querySelector('loading-icon')).toBe(null); + expect(component.$el.querySelector('loading-icon')).toBe(null); - expect(component.$el.querySelector('.stage-column-list')).toBeDefined(); - done(); - }, 0); + expect(component.$el.querySelector('.stage-column-list')).toBeDefined(); }); }); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 63986b6c0db..e90593e0f40 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -27,26 +27,30 @@ describe('pipeline graph job component', () => { }); describe('name with link', () => { - it('should render the job name and status with a link', () => { + it('should render the job name and status with a link', (done) => { const component = new JobComponent({ propsData: { job: mockJob, }, }).$mount(); - const link = component.$el.querySelector('a'); + Vue.nextTick(() => { + const link = component.$el.querySelector('a'); - expect(link.getAttribute('href')).toEqual(mockJob.status.details_path); + expect(link.getAttribute('href')).toEqual(mockJob.status.details_path); - expect( - link.getAttribute('data-original-title'), - ).toEqual(`${mockJob.name} - ${mockJob.status.label}`); + expect( + link.getAttribute('data-original-title'), + ).toEqual(`${mockJob.name} - ${mockJob.status.label}`); - expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined(); + expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined(); - expect( - component.$el.querySelector('.ci-status-text').textContent.trim(), - ).toEqual(mockJob.name); + expect( + component.$el.querySelector('.ci-status-text').textContent.trim(), + ).toEqual(mockJob.name); + + done(); + }); }); }); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 0bcc3905702..d74b1281668 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelineUrlComp from '~/pipelines/components/pipeline_url'; +import pipelineUrlComp from '~/pipelines/components/pipeline_url.vue'; describe('Pipeline Url Component', () => { let PipelineUrlComponent; diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js index b31a7c28ebe..c82658b9262 100644 --- a/spec/javascripts/raven/raven_config_spec.js +++ b/spec/javascripts/raven/raven_config_spec.js @@ -140,24 +140,6 @@ describe('RavenConfig', () => { }); }); - describe('bindRavenErrors', () => { - let $document; - let $; - - beforeEach(() => { - $document = jasmine.createSpyObj('$document', ['on']); - $ = jasmine.createSpy('$').and.returnValue($document); - - window.$ = $; - - RavenConfig.bindRavenErrors(); - }); - - it('should call .on', function () { - expect($document.on).toHaveBeenCalledWith('ajaxError.raven', RavenConfig.handleRavenErrors); - }); - }); - describe('handleRavenErrors', () => { let event; let req; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index da9dff18ada..2c3d0ddff28 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -7,6 +7,18 @@ const url = '/root/acets-review-apps/environments/15/deployments/1/metrics'; const metricsMockData = { success: true, metrics: { + memory_before: [ + { + metric: {}, + value: [1495785220.607, '9572875.906976745'], + }, + ], + memory_after: [ + { + metric: {}, + value: [1495787020.607, '4485853.130206379'], + }, + ], memory_values: [ { metric: {}, @@ -39,7 +51,7 @@ const createComponent = () => { const messages = { loadingMetrics: 'Loading deployment statistics.', - hasMetrics: 'Deployment memory usage:', + hasMetrics: 'Memory usage unchanged from 0MB to 0MB', loadFailed: 'Failed to load deployment statistics.', metricsUnavailable: 'Deployment statistics are not available currently.', }; @@ -89,17 +101,52 @@ describe('MemoryUsage', () => { }); }); + describe('computed', () => { + describe('memoryChangeType', () => { + it('should return "increased" if memoryFrom value is less than memoryTo value', () => { + vm.memoryFrom = 4.28; + vm.memoryTo = 9.13; + + expect(vm.memoryChangeType).toEqual('increased'); + }); + + it('should return "decreased" if memoryFrom value is less than memoryTo value', () => { + vm.memoryFrom = 9.13; + vm.memoryTo = 4.28; + + expect(vm.memoryChangeType).toEqual('decreased'); + }); + + it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => { + vm.memoryFrom = 1; + vm.memoryTo = 1; + + expect(vm.memoryChangeType).toEqual('unchanged'); + }); + }); + }); + describe('methods', () => { const { metrics, deployment_time } = metricsMockData; + describe('getMegabytes', () => { + it('should return Megabytes from provided Bytes value', () => { + const memoryInBytes = '9572875.906976745'; + + expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13'); + }); + }); + describe('computeGraphData', () => { it('should populate sparkline graph', () => { vm.computeGraphData(metrics, deployment_time); - const { hasMetrics, memoryMetrics, deploymentTime } = vm; + const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm; expect(hasMetrics).toBeTruthy(); expect(memoryMetrics.length > 0).toBeTruthy(); expect(deploymentTime).toEqual(deployment_time); + expect(memoryFrom).toEqual('9.13'); + expect(memoryTo).toEqual('4.28'); }); }); 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 index d043ad38b8b..732b516badd 100644 --- 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 @@ -5,7 +5,7 @@ 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 createComponent = (customConfig = {}) => { const Component = Vue.extend(readyToMergeComponent); const mr = { isPipelineActive: false, @@ -17,8 +17,12 @@ const createComponent = () => { sha: '12345678', commitMessage, commitMessageWithDescription, + shouldRemoveSourceBranch: true, + canRemoveSourceBranch: false, }; + Object.assign(mr, customConfig.mr); + const service = { merge() {}, poll() {}, @@ -51,7 +55,6 @@ describe('MRWidgetReadyToMerge', () => { describe('data', () => { it('should have default data', () => { - expect(vm.removeSourceBranch).toBeTruthy(true); expect(vm.mergeWhenBuildSucceeds).toBeFalsy(); expect(vm.useCommitMessageWithDescription).toBeFalsy(); expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy(); @@ -166,6 +169,36 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); }); + + describe('Remove source branch checkbox', () => { + describe('when user can merge but cannot delete branch', () => { + it('isRemoveSourceBranchButtonDisabled should be true', () => { + expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); + }); + + it('should be disabled in the rendered output', () => { + const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); + }); + }); + + describe('when user can merge and can delete branch', () => { + beforeEach(() => { + this.customVm = createComponent({ + mr: { canRemoveSourceBranch: true }, + }); + }); + + it('isRemoveSourceBranchButtonDisabled should be false', () => { + expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + }); + + it('should be enabled in rendered output', () => { + const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBeNull(); + }); + }); + }); }); describe('methods', () => { diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index bdc18243a15..3a0c50b750f 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -2,6 +2,7 @@ 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 notify from '~/lib/utils/notify'; import mockData from './mock_data'; const createComponent = () => { @@ -107,6 +108,8 @@ describe('mrWidgetOptions', () => { it('should tell service to check status', (done) => { spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData)); spyOn(vm.mr, 'setData'); + spyOn(vm, 'handleNotification'); + let isCbExecuted = false; const cb = () => { isCbExecuted = true; @@ -117,6 +120,7 @@ describe('mrWidgetOptions', () => { setTimeout(() => { expect(vm.service.checkStatus).toHaveBeenCalled(); expect(vm.mr.setData).toHaveBeenCalled(); + expect(vm.handleNotification).toHaveBeenCalledWith(mockData); expect(isCbExecuted).toBeTruthy(); done(); }, 333); @@ -254,6 +258,39 @@ describe('mrWidgetOptions', () => { }); }); + describe('handleNotification', () => { + const data = { + ci_status: 'running', + title: 'title', + pipeline: { details: { status: { label: 'running-label' } } }, + }; + + beforeEach(() => { + spyOn(notify, 'notifyMe'); + + vm.mr.ciStatus = 'failed'; + vm.mr.gitlabLogo = 'logo.png'; + }); + + it('should call notifyMe', () => { + vm.handleNotification(data); + + expect(notify.notifyMe).toHaveBeenCalledWith( + 'Pipeline running-label', + 'Pipeline running-label for "title"', + 'logo.png', + ); + }); + + it('should not call notifyMe if the status has not changed', () => { + vm.mr.ciStatus = data.ci_status; + + vm.handleNotification(data); + + expect(notify.notifyMe).not.toHaveBeenCalled(); + }); + }); + describe('resumePolling', () => { it('should call stopTimer on pollingInterval', () => { spyOn(vm.pollingInterval, 'resume'); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js new file mode 100644 index 00000000000..1bf8916b3d0 --- /dev/null +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; +import headerCi from '~/vue_shared/components/header_ci_component.vue'; + +describe('Header CI Component', () => { + let HeaderCi; + let vm; + let props; + + beforeEach(() => { + HeaderCi = Vue.extend(headerCi); + + props = { + status: { + group: 'failed', + icon: 'ci-status-failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + itemName: 'job', + itemId: 123, + time: '2017-05-08T14:57:39.781Z', + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + actions: [ + { + label: 'Retry', + path: 'path', + type: 'button', + cssClass: 'btn', + }, + { + label: 'Go', + path: 'path', + type: 'link', + cssClass: 'link', + }, + ], + }; + + vm = new HeaderCi({ + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render status badge', () => { + expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); + expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); + expect( + vm.$el.querySelector('.ci-failed').getAttribute('href'), + ).toEqual(props.status.details_path); + }); + + it('should render item name and id', () => { + expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); + }); + + it('should render timeago date', () => { + expect(vm.$el.querySelector('time')).toBeDefined(); + }); + + it('should render user icon and name', () => { + expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); + }); + + it('should render provided actions', () => { + expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); + expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); + expect(vm.$el.querySelector('.link').tagName).toEqual('A'); + expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); + expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js new file mode 100644 index 00000000000..4bbaff561fc --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -0,0 +1,121 @@ +import Vue from 'vue'; +import fieldComponent from '~/vue_shared/components/markdown/field.vue'; + +describe('Markdown field component', () => { + let vm; + + beforeEach(() => { + vm = new Vue({ + render(createElement) { + return createElement( + fieldComponent, + { + props: { + markdownPreviewUrl: '/preview', + markdownDocs: '/docs', + }, + }, + [ + createElement('textarea', { + slot: 'textarea', + }), + ], + ); + }, + }); + }); + + it('creates a new instance of GL form', (done) => { + spyOn(gl, 'GLForm'); + vm.$mount(); + + Vue.nextTick(() => { + expect( + gl.GLForm, + ).toHaveBeenCalled(); + + done(); + }); + }); + + describe('mounted', () => { + beforeEach((done) => { + vm.$mount(); + + Vue.nextTick(done); + }); + + it('renders textarea inside backdrop', () => { + expect( + vm.$el.querySelector('.zen-backdrop textarea'), + ).not.toBeNull(); + }); + + describe('markdown preview', () => { + let previewLink; + + beforeEach(() => { + spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => { + resolve({ + json() { + return { + body: '<p>markdown preview</p>', + }; + }, + }); + })); + + previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a'); + }); + + it('sets preview link as active', (done) => { + previewLink.click(); + + Vue.nextTick(() => { + expect( + previewLink.parentNode.classList.contains('active'), + ).toBeTruthy(); + + done(); + }); + }); + + it('shows preview loading text', (done) => { + previewLink.click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.md-preview').textContent.trim(), + ).toContain('Loading...'); + + done(); + }); + }); + + it('renders markdown preview', (done) => { + previewLink.click(); + + setTimeout(() => { + expect( + vm.$el.querySelector('.md-preview').innerHTML, + ).toContain('<p>markdown preview</p>'); + + done(); + }); + }); + + it('renders GFM with jQuery', (done) => { + spyOn($.fn, 'renderGFM'); + previewLink.click(); + + setTimeout(() => { + expect( + $.fn.renderGFM, + ).toHaveBeenCalled(); + + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js new file mode 100644 index 00000000000..7110ff36937 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js @@ -0,0 +1,67 @@ +import Vue from 'vue'; +import headerComponent from '~/vue_shared/components/markdown/header.vue'; + +describe('Markdown field header component', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(headerComponent); + + vm = new Component({ + propsData: { + previewMarkdown: false, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders markdown buttons', () => { + expect( + vm.$el.querySelectorAll('.js-md').length, + ).toBe(7); + }); + + it('renders `write` link as active when previewMarkdown is false', () => { + expect( + vm.$el.querySelector('li:nth-child(1)').classList.contains('active'), + ).toBeTruthy(); + }); + + it('renders `preview` link as active when previewMarkdown is true', (done) => { + vm.previewMarkdown = true; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('li:nth-child(2)').classList.contains('active'), + ).toBeTruthy(); + + done(); + }); + }); + + it('emits toggle markdown event when clicking preview', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('li:nth-child(2) a').click(); + + expect( + vm.$emit, + ).toHaveBeenCalledWith('toggle-markdown'); + }); + + it('blurs preview link after click', (done) => { + const link = vm.$el.querySelector('li:nth-child(2) a'); + spyOn(HTMLElement.prototype, 'blur'); + + link.click(); + + setTimeout(() => { + expect( + link.blur, + ).toHaveBeenCalled(); + + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js new file mode 100644 index 00000000000..bf28019ef24 --- /dev/null +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import '~/lib/utils/datetime_utility'; + +describe('Time ago with tooltip component', () => { + let TimeagoTooltip; + let vm; + + beforeEach(() => { + TimeagoTooltip = Vue.extend(timeagoTooltip); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render timeago with a bootstrap tooltip', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + }, + }).$mount(); + + expect(vm.$el.tagName).toEqual('TIME'); + expect(vm.$el.classList.contains('js-timeago')).toEqual(true); + expect( + vm.$el.getAttribute('data-original-title'), + ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z')); + expect(vm.$el.getAttribute('data-placement')).toEqual('top'); + + const timeago = gl.utils.getTimeago(); + + expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z')); + }); + + it('should render tooltip placed in bottom', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + tooltipPlacement: 'bottom', + }, + }).$mount(); + + expect(vm.$el.getAttribute('data-placement')).toEqual('bottom'); + }); + + it('should render short format class', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + shortFormat: true, + }, + }).$mount(); + + expect(vm.$el.classList.contains('js-short-timeago')).toEqual(true); + }); + + it('should render provided html class', () => { + vm = new TimeagoTooltip({ + propsData: { + time: '2017-05-08T14:57:39.781Z', + cssClass: 'foo', + }, + }).$mount(); + + expect(vm.$el.classList.contains('foo')).toEqual(true); + }); +}); |